Skip to content

Conversation

@zhravan
Copy link
Collaborator

@zhravan zhravan commented Oct 24, 2025

Description

This PR significantly enhances the dashboard with an improved user experience through drag-and-drop card reordering, interactive charts, and expanded system information.

image

Key Changes

Dashboard UX Improvements

  • Draggable Grid Layout: Implemented drag-and-drop functionality for dashboard cards using @dnd-kit
    • Cards can be reordered via drag-and-drop
    • Layout order persists in localStorage
    • Added "Reset Layout" button to restore default order
    • Added drag hint banner for user guidance

Enhanced Visualizations

  • Load Average Card: Replaced text display with interactive bar chart showing 1min, 5min, and 15min load averages
  • Memory Usage Card: Added doughnut chart visualization with used/free memory breakdown
  • System Info Card: Redesigned with grid layout and comprehensive system details

Extended System Metrics

Added new system information fields:

  • Hostname
  • CPU Cores count
  • Kernel Version
  • Architecture
  • Enhanced memory display with total/used/free breakdown

Backend Changes

  • Extended SystemStats struct in Go backend to include hostname, CPU cores, kernel version, and architecture
  • Improved system stats collection logic

Internationalization

  • Updated locale files for English, Spanish, French, Kannada, and Malayalam
  • Added translations for new system metadata labels

New Components

  • DraggableGrid: Reusable drag-and-drop grid component with persistence
  • BarChartComponent: Configurable bar chart component using recharts
  • DoughnutChartComponent: Donut chart component for circular visualizations

Dependencies

  • Added @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities for drag-and-drop functionality

Scope of Change

  • View
  • API
  • CLI
  • Infra/Deployment
  • Docs
  • Other

Testing Notes

  • Test drag-and-drop functionality across different dashboard cards
  • Verify layout persistence after page refresh
  • Verify reset layout functionality
  • Check chart rendering with different data values
  • Test responsiveness across different screen sizes
  • Verify all locale translations display correctly

Summary by CodeRabbit

Release Notes

  • New Features
    • Draggable dashboard layout: Users can now reorder dashboard cards and save custom layouts; a reset button restores default layout.
    • Enhanced CPU monitoring: New CPU usage card displays overall usage and per-core metrics with visual indicators.
    • Improved visualizations: System metrics now use interactive charts (bar charts for load average, doughnut chart for memory usage).
    • Extended system information: Dashboard now displays hostname, kernel version, and system architecture details.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 24, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

The dashboard undergoes a comprehensive redesign introducing draggable grid layouts for system statistics cards, enhanced metric visualizations via bar and doughnut charts, and expanded backend system data collection including hostname, kernel version, architecture, and per-core CPU metrics.

Changes

Cohort / File(s) Change Summary
Drag-and-drop infrastructure
view/components/ui/draggable-grid.tsx, view/package.json
Introduces DraggableGrid component with @dnd-kit integration for grid reordering; adds localStorage-based persistence and resetKey support. Added dependencies: @dnd-kit/core, @dnd-kit/sortable, @dnd-kit/utilities.
Dashboard page refactoring
view/app/dashboard/page.tsx
Replaces static layout with draggable grid; introduces useDashboard hook; adds DashboardHeader with reset layout button and DragHintBanner; restructures MonitoringSection to support draggable layout and layout change callbacks.
System stats component migration
view/app/dashboard/components/system/system-stats.tsx
Migrates from static layout to draggable grid with systemStatsItems mapping; adds CPUUsageCard; integrates DraggableGrid and DraggableItem usage.
CPU usage visualization
view/app/dashboard/components/system/cpu-usage.tsx, view/app/dashboard/components/system/skeletons/cpu-usage.tsx
New CPUUsageCard component rendering per-core CPU metrics with bar chart; includes skeleton variant with loading state.
Load average refactoring
view/app/dashboard/components/system/load-average.tsx, view/app/dashboard/components/system/skeletons/load-average.tsx
Refactored to use SystemMetricCard wrapper and BarChartComponent; migrated to useSystemMetric hook; updated props to accept nullable systemStats.
Memory usage refactoring
view/app/dashboard/components/system/memory-usage.tsx, view/app/dashboard/components/system/skeletons/memory-usage.tsx
Replaced Card layout with SystemMetricCard and DoughnutChartComponent; introduced memory free calculation; updated to useSystemMetric hook; nullable systemStats prop.
System info refactoring
view/app/dashboard/components/system/system-info.tsx, view/app/dashboard/components/system/skeletons/system-info.tsx
Migrated from fixed fields to data-driven grid with SystemInfoItem; moved skeleton to dedicated module; added hostname, kernel version, architecture fields; nullable systemStats prop.
Disk usage refactoring
view/app/dashboard/components/system/disk-usage.tsx, view/app/dashboard/components/system/skeletons/disk-usage.tsx
Replaced Card layout with SystemMetricCard wrapper; integrated useSystemMetric hook; separated skeleton content; updated percentage formatting.
Chart components
view/components/ui/bar-chart-component.tsx, view/components/ui/doughnut-chart-component.tsx
Introduced new BarChartComponent and DoughnutChartComponent using recharts; includes configurable data, sizing, and tooltip support.
System metric utilities
view/app/dashboard/components/system/system-metric-card.tsx, view/app/dashboard/components/utils/constants.ts, view/app/dashboard/components/utils/utils.ts
Added SystemMetricCard wrapper component; defined CHART_COLORS and DEFAULT_METRICS constants; introduced formatGB, formatPercentage, and chart data/config creation utilities.
Hooks
view/app/dashboard/hooks/use-dashboard.ts, view/app/dashboard/hooks/use-system-metric.ts, view/app/dashboard/hooks/use-smtp-banner.ts
New useDashboard hook managing feature flags, layout state, and SMTP config; new useSystemMetric generic hook for metric data extraction; refactored use-smtp-banner hook.
Skeleton barrel export
view/app/dashboard/components/system/skeletons/index.tsx
Consolidates skeleton exports from CPU, Load Average, Memory, Disk, and System Info submodules.
SMTP banner
view/app/dashboard/components/smtp-banner.tsx
Abstracted logic to use-smtp-banner hook; simplified component to delegate state and actions.
Backend system stats
api/internal/features/dashboard/system_stats.go, api/internal/features/dashboard/types.go
Extended SystemStats with hostname, kernel version, architecture; introduced CPUStats and CPUCore types; added per-core CPU usage collection via getCPUStats; expanded metadata gathering.
Frontend types
view/redux/types/monitor.ts
Extended SystemStatsType with hostname, cpu_cores, kernel_version, architecture; added nested cpu property of type CPUStats with per-core entries.
Localization
view/lib/i18n/locales/en.json, view/lib/i18n/locales/es.json, view/lib/i18n/locales/fr.json, view/lib/i18n/locales/kn.json, view/lib/i18n/locales/ml.json
Added CPU usage section with title, overall, usage, cores, perCore keys; extended system metadata with hostname, kernelVersion, architecture, memory; added nested labels object for all system fields.
Configuration
.gitignore, api/api/versions.json
Added api/nixopus-api to ignore list; updated release timestamp.

Sequence Diagram

sequenceDiagram
    participant User
    participant Dashboard as Dashboard Page
    participant Hook as useDashboard
    participant API as API
    participant DragGrid as DraggableGrid
    participant LS as localStorage

    User->>Dashboard: Load dashboard
    Dashboard->>Hook: Call useDashboard()
    Hook->>API: Fetch system stats, containers, SMTP config
    API-->>Hook: Return data
    Hook->>LS: Check for saved layout order
    Hook-->>Dashboard: Return data + UI state + actions
    Dashboard->>Dashboard: Mount with layoutResetKey
    Dashboard->>DragGrid: Render with items + storageKey
    DragGrid->>LS: Load saved item order
    LS-->>DragGrid: Return saved order or undefined
    DragGrid->>DragGrid: Merge saved order with items
    
    Note over User,DragGrid: User drags item
    User->>DragGrid: Drag and drop item
    DragGrid->>DragGrid: Update local order
    DragGrid->>LS: Persist new order
    DragGrid->>Dashboard: Call onLayoutChange callback
    
    Note over User,Dashboard: User clicks Reset Layout
    User->>Dashboard: Click reset button
    Dashboard->>Hook: Call handleResetLayout()
    Hook->>LS: Clear saved order + dismiss hint
    Hook->>Dashboard: Return new layoutResetKey
    Dashboard->>DragGrid: Re-render with new resetKey
    DragGrid->>LS: Load (finds none, uses defaults)
    DragGrid->>DragGrid: Reset to default item order
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • Backend data collection: Review the new getCPUStats function and per-core CPU metrics extraction logic in api/internal/features/dashboard/system_stats.go to ensure accurate syscall usage and fallback handling.
  • Draggable grid implementation: Verify the @dnd-kit integration, localStorage persistence, and resetKey/order sync logic in view/components/ui/draggable-grid.tsx works correctly across page reloads and layout changes.
  • Type alignment: Ensure SystemStatsType changes in both frontend (view/redux/types/monitor.ts) and backend (api/internal/features/dashboard/types.go) are correctly serialized and deserialized.
  • Hook complexity: Review view/app/dashboard/hooks/use-dashboard.ts for proper feature flag integration, SMTP config query skip conditions, and localStorage key management.
  • Component refactoring: Verify all system metric card refactors (CPU, Load Average, Memory, Disk, System Info) correctly handle null systemStats and loading states via useSystemMetric.
  • Localization coverage: Confirm all new localization keys are present and correctly translated across all five language files (en, es, fr, kn, ml).

Possibly related PRs

Poem

🐰 A rabbit hops through draggable cards so neat,
CPU cores dance to a bar-chart beat,
Doughnuts of memory spin round and round,
With dnd-kit magic, the best layout's found!
Reset the hint and persist the way,
Dashboard's rebirth celebrates today!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.41% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "feat: enhance dashboard with draggable layout, charts, and extended system metrics" directly summarizes the three major changes present throughout the changeset. The changes include a new DraggableGrid component enabling card reordering with localStorage persistence, multiple new chart components (BarChartComponent and DoughnutChartComponent) for visualizing system metrics, and significant backend and frontend extensions to capture and display additional system information (hostname, CPU cores, kernel version, architecture). The title is concise, uses standard commit conventions, and is specific enough that a developer reviewing history would immediately understand these dashboard enhancements without excessive detail.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2c2e7b and 6bc0df6.

📒 Files selected for processing (9)
  • api/internal/features/dashboard/system_stats.go (3 hunks)
  • view/app/dashboard/components/smtp-banner.tsx (1 hunks)
  • view/app/dashboard/components/system/cpu-usage.tsx (1 hunks)
  • view/app/dashboard/components/system/disk-usage.tsx (2 hunks)
  • view/app/dashboard/components/system/load-average.tsx (1 hunks)
  • view/app/dashboard/components/system/memory-usage.tsx (1 hunks)
  • view/app/dashboard/components/system/system-info.tsx (1 hunks)
  • view/app/dashboard/components/utils/utils.ts (1 hunks)
  • view/app/dashboard/hooks/use-smtp-banner.ts (1 hunks)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
api/internal/features/dashboard/init.go (1)

31-55: Restart logic uses a canceled context (will not actually restart)

UpdateConfig/SetOperations call Stop() then Start(), but Start() reuses m.ctx created in the constructor. After cancel(), the context remains canceled and the loop exits immediately. Create a fresh context in Start() (or a helper) before spawning the goroutine.

Minimal fix sketch:

 func (m *DashboardMonitor) Start() {
-  go func() {
+  // refresh context for a new run
+  if m.cancel != nil {
+    // no-op if already fresh
+  }
+  m.ctx, m.cancel = context.WithCancel(context.Background())
+  go func() {
     ticker := time.NewTicker(m.Interval)
     ...
   }()
 }

Also ensure Stop() is idempotent and doesn’t double-close.

Also applies to: 65-74, 89-98

view/app/dashboard/hooks/use-monitor.ts (1)

109-115: Duplicate reconnect timers and untracked timeout can cause double starts and leaks.

On error you call setTimeout(startMonitoring, 5000) and you also have a reconnect effect that schedules another timer via reconnectTimeoutRef. This can fire twice and the untracked timeout cannot be cleared on unmount.

Apply this diff to rely solely on the tracked reconnect effect:

         if (isMonitoring) {
           stopMonitoring();
-          setTimeout(startMonitoring, 5000);
         }
🧹 Nitpick comments (11)
view/app/dashboard/components/charts/time-series-chart.tsx (1)

29-35: Don’t hardcode 'en-US' for time labels

Use the viewer’s locale (or a prop) to localize time on the X-axis.

Apply this diff:

-  const chartData = data.map(point => ({
-    time: new Date(point.timestamp).toLocaleTimeString('en-US', {
+  const chartData = data.map(point => ({
+    time: new Date(point.timestamp).toLocaleTimeString(undefined, {
       hour: '2-digit',
       minute: '2-digit',
       second: '2-digit'
     }),
     [dataKey]: point.value
   }));
view/app/dashboard/components/charts/caddy-charts.tsx (1)

13-41: Localize titles/descriptions

Hook these strings to i18n (e.g., useTranslations) instead of hardcoding English so the new locale keys are actually used.

Example:

+'use client';
+import { useTranslations } from 'next-intl';
 
 export function CaddyCharts({ data }: CaddyChartsProps) {
-  return (
+  const t = useTranslations('dashboard.caddy'); // or another namespace
+  return (
     <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
       <TimeSeriesChart
-        title="Request Rate"
-        description="HTTP requests per second"
+        title={t('requestRateTitle')}
+        description={t('requestRateDesc')}
         data={data.requestRate}
         dataKey="requests"
         color="hsl(var(--chart-1))"
         unit=" req/s"
         formatValue={(value) => `${value.toFixed(2)} req/s`}
       />
       <TimeSeriesChart
-        title="Error Rate"
-        description="Percentage of failed requests"
+        title={t('errorRateTitle')}
+        description={t('errorRateDesc')}
         data={data.errorRate}
         dataKey="errors"
         color="hsl(var(--destructive))"
         unit="%"
         formatValue={(value) => `${value.toFixed(2)}%`}
       />
       <TimeSeriesChart
-        title="Average Latency"
-        description="Response time in milliseconds"
+        title={t('avgLatencyTitle')}
+        description={t('avgLatencyDesc')}
         data={data.avgLatency}
         dataKey="latency"
         color="hsl(var(--chart-3))"
         unit="ms"
         formatValue={(value) => `${value.toFixed(0)}ms`}
       />
     </div>
   );
}

Add the corresponding keys in locales.

view/app/dashboard/components/charts/server-specific-charts.tsx (1)

30-33: Consider simplifying the sign prefix logic.

The conditional sign prefix can be simplified using a ternary or by relying on JavaScript's default number-to-string conversion with explicit sign handling.

Consider this alternative:

         formatValue={(value) => {
-          const sign = value >= 0 ? '+' : '';
-          return `${sign}${value.toFixed(3)} GB/hr`;
+          return `${value >= 0 ? '+' : ''}${value.toFixed(3)} GB/hr`;
         }}
view/app/dashboard/components/system/caddy-metrics.tsx (2)

57-59: Consider sorting applications before displaying "top 5".

The code displays the first 5 applications from perApplication without any sorting, making the selection arbitrary. Consider sorting by a relevant metric (e.g., request rate, error rate) to show the most important applications.

Apply this diff to sort by request rate:

                 {Object.entries(caddyMetrics.perApplication)
+                  .sort(([, a], [, b]) => b.requestRate - a.requestRate)
                   .slice(0, 5)
                   .map(([domain, metrics]) => (

70-73: Document or make configurable the error rate threshold.

The 5% error rate threshold for highlighting errors in red is hardcoded. Consider extracting this to a constant or making it configurable for better maintainability.

+const ERROR_RATE_THRESHOLD = 5;
+
 const CaddyMetricsCard: React.FC<CaddyMetricsCardProps> = ({ caddyMetrics }) => {
   // ...
                           <span
-                            className={`text-xs ${metrics.errorRate > 5 ? 'text-red-500' : 'text-muted-foreground'}`}
+                            className={`text-xs ${metrics.errorRate > ERROR_RATE_THRESHOLD ? 'text-red-500' : 'text-muted-foreground'}`}
                           >
view/app/dashboard/hooks/use-monitor.ts (2)

31-31: Use ReturnType in client code.

NodeJS.Timeout is Node-specific; in the browser it’s a number. Use a cross‑env type to avoid TS friction in Next.js client modules.

-  const reconnectTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
+  const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);

91-108: DRY up capped append logic for time-series.

The slice-and-append pattern repeats. Extract a tiny helper to reduce duplication and off‑by‑one risk.

+ const appendPoint = (arr: TimeSeriesDataPoint[], p: TimeSeriesDataPoint) =>
+   [...arr.slice(-MAX_HISTORY_POINTS + 1), p];
...
- setAdvancedTimeSeries(prev => {
-   const newData = { ...prev };
-   if (data.pageFaults?.faultsPerSec !== undefined) {
-     newData.pageFaultsPerSec = [
-       ...prev.pageFaultsPerSec.slice(-MAX_HISTORY_POINTS + 1),
-       { timestamp, value: data.pageFaults.faultsPerSec }
-     ];
-   }
-   if (data.disk?.growthRate !== undefined) {
-     newData.diskGrowthRate = [
-       ...prev.diskGrowthRate.slice(-MAX_HISTORY_POINTS + 1),
-       { timestamp, value: data.disk.growthRate }
-     ];
-   }
-   return newData;
- });
+ setAdvancedTimeSeries(prev => ({
+   pageFaultsPerSec: data.pageFaults?.faultsPerSec !== undefined
+     ? appendPoint(prev.pageFaultsPerSec, { timestamp, value: data.pageFaults.faultsPerSec })
+     : prev.pageFaultsPerSec,
+   diskGrowthRate: data.disk?.growthRate !== undefined
+     ? appendPoint(prev.diskGrowthRate, { timestamp, value: data.disk.growthRate })
+     : prev.diskGrowthRate
+ }));
...
- setCaddyTimeSeries(prev => ({
-   requestRate: [
-     ...prev.requestRate.slice(-MAX_HISTORY_POINTS + 1),
-     { timestamp, value: data.overall.requestRate }
-   ],
-   errorRate: [
-     ...prev.errorRate.slice(-MAX_HISTORY_POINTS + 1),
-     { timestamp, value: data.overall.errorRate }
-   ],
-   avgLatency: [
-     ...prev.avgLatency.slice(-MAX_HISTORY_POINTS + 1),
-     { timestamp, value: data.overall.avgLatency }
-   ]
- }));
+ setCaddyTimeSeries(prev => ({
+   requestRate: appendPoint(prev.requestRate, { timestamp, value: data.overall.requestRate }),
+   errorRate: appendPoint(prev.errorRate, { timestamp, value: data.overall.errorRate }),
+   avgLatency: appendPoint(prev.avgLatency, { timestamp, value: data.overall.avgLatency })
+ }));

Also applies to: 64-86

api/internal/features/dashboard/caddy_metrics.go (2)

64-82: Prometheus text parsing is fragile; prefer expfmt.

Manual splitting breaks with escaped quotes, commas in label values, exemplars, or metadata lines. Use the official parser.

Illustrative replacement:

import (
  "github.com/prometheus/common/expfmt"
  io_prom "github.com/prometheus/client_model/go"
)

func (m *DashboardMonitor) fetchCaddyMetrics(url string) (map[string]*io_prom.MetricFamily, error) {
  // http GET same as today...
  var parser expfmt.TextParser
  fam, err := parser.TextToMetricFamilies(bytes.NewReader(body))
  if err != nil { return nil, fmt.Errorf("parse metrics: %w", err) }
  return fam, nil
}

Then iterate metric families to aggregate safely. This also gives you sum/count directly for histograms/summaries.

Also applies to: 85-121


41-62: Reuse an http.Client or use context.WithTimeout.

Creating a new client per call is fine at low rates but avoid per-scrape allocation; also add User-Agent for observability.

- client := &http.Client{
-   Timeout: 5 * time.Second,
- }
- resp, err := client.Get(url)
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
+ req.Header.Set("User-Agent", "nixopus-dashboard/1.0")
+ resp, err := http.DefaultClient.Do(req)
api/internal/features/dashboard/types.go (2)

109-122: Document units for Caddy metrics fields.

Clarify units to stay consistent with UI (e.g., RequestRate req/s, ErrorRate %, AvgLatency ms).

 type CaddyAppMetrics struct {
-  RequestRate   float64 `json:"requestRate"` // req/second
-  ErrorRate     float64 `json:"errorRate"`   // percentage of 4xx/5xx responses
-  AvgLatency    float64 `json:"avgLatency"`  // milliseconds
+  RequestRate   float64 `json:"requestRate"` // requests per second (rps)
+  ErrorRate     float64 `json:"errorRate"`   // error percentage over window [0..100]
+  AvgLatency    float64 `json:"avgLatency"`  // average latency in milliseconds over window
   RequestCount  uint64  `json:"requestCount"`
   Error4xxCount uint64  `json:"error4xxCount"`
   Error5xxCount uint64  `json:"error5xxCount"`
 }

85-94: Clarify GrowthRate units in JSON contract.

Make the GB/hr unit explicit in the struct tag comment to help the UI.

 type DiskStats struct {
   Total      float64     `json:"total"`
   Used       float64     `json:"used"`
   Available  float64     `json:"available"`
   Percentage float64     `json:"percentage"`
   MountPoint string      `json:"mountPoint"`
   AllMounts  []DiskMount `json:"allMounts"`
-  GrowthRate float64     `json:"growthRate"` // GB/hr
+  GrowthRate float64     `json:"growthRate"` // disk growth rate in GB per hour
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 29edb31 and 6d5becb.

📒 Files selected for processing (21)
  • .gitignore (1 hunks)
  • api/Dockerfile.dev (1 hunks)
  • api/internal/features/dashboard/caddy_metrics.go (1 hunks)
  • api/internal/features/dashboard/init.go (1 hunks)
  • api/internal/features/dashboard/system_stats.go (3 hunks)
  • api/internal/features/dashboard/types.go (3 hunks)
  • view/app/dashboard/components/charts/caddy-charts.tsx (1 hunks)
  • view/app/dashboard/components/charts/server-specific-charts.tsx (1 hunks)
  • view/app/dashboard/components/charts/time-series-chart.tsx (1 hunks)
  • view/app/dashboard/components/system/caddy-metrics.tsx (1 hunks)
  • view/app/dashboard/components/system/cpu-cores.tsx (1 hunks)
  • view/app/dashboard/components/system/server-metrics.tsx (1 hunks)
  • view/app/dashboard/components/system/system-stats.tsx (3 hunks)
  • view/app/dashboard/hooks/use-monitor.ts (4 hunks)
  • view/app/dashboard/page.tsx (5 hunks)
  • view/lib/i18n/locales/en.json (1 hunks)
  • view/lib/i18n/locales/es.json (1 hunks)
  • view/lib/i18n/locales/fr.json (1 hunks)
  • view/lib/i18n/locales/kn.json (1 hunks)
  • view/lib/i18n/locales/ml.json (1 hunks)
  • view/redux/types/monitor.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (13)
api/internal/features/dashboard/init.go (1)
api/internal/features/dashboard/types.go (1)
  • GetCaddyMetrics (20-20)
view/app/dashboard/components/charts/caddy-charts.tsx (2)
view/app/dashboard/hooks/use-monitor.ts (1)
  • CaddyTimeSeriesData (13-17)
view/app/dashboard/components/charts/time-series-chart.tsx (1)
  • TimeSeriesChart (18-106)
view/app/dashboard/components/system/caddy-metrics.tsx (1)
view/redux/types/monitor.ts (1)
  • CaddyMetrics (83-87)
view/app/dashboard/components/system/server-metrics.tsx (2)
view/components/ui/card.tsx (4)
  • Card (58-58)
  • CardHeader (58-58)
  • CardTitle (58-58)
  • CardContent (58-58)
view/components/ui/skeleton.tsx (1)
  • Skeleton (13-13)
view/app/dashboard/hooks/use-monitor.ts (2)
view/hooks/socket-provider.tsx (1)
  • useWebSocket (190-198)
view/redux/types/monitor.ts (3)
  • ContainerData (1-18)
  • SystemStatsType (89-98)
  • CaddyMetrics (83-87)
view/app/dashboard/components/charts/time-series-chart.tsx (1)
view/app/dashboard/hooks/use-monitor.ts (1)
  • TimeSeriesDataPoint (8-11)
api/internal/features/dashboard/caddy_metrics.go (2)
api/internal/features/dashboard/types.go (4)
  • DashboardMonitor (34-49)
  • GetCaddyMetrics (20-20)
  • CaddyMetrics (109-113)
  • CaddyAppMetrics (115-122)
api/internal/features/logger/init.go (1)
  • Error (22-22)
api/internal/features/dashboard/system_stats.go (1)
api/internal/features/dashboard/types.go (3)
  • DashboardMonitor (34-49)
  • CPUCoreStats (95-101)
  • PageFaultStats (103-107)
view/app/dashboard/components/system/system-stats.tsx (2)
view/app/dashboard/components/system/cpu-cores.tsx (1)
  • CPUCoresCardSkeleton (48-67)
view/app/dashboard/components/system/server-metrics.tsx (1)
  • ServerMetricsCardSkeleton (79-94)
view/app/dashboard/components/charts/server-specific-charts.tsx (2)
view/app/dashboard/hooks/use-monitor.ts (1)
  • ServerSpecificTimeSeriesData (19-22)
view/app/dashboard/components/charts/time-series-chart.tsx (1)
  • TimeSeriesChart (18-106)
view/app/dashboard/components/system/cpu-cores.tsx (1)
view/redux/types/monitor.ts (1)
  • CPUCoreStats (60-66)
view/app/dashboard/page.tsx (2)
view/app/dashboard/components/charts/caddy-charts.tsx (1)
  • CaddyCharts (10-44)
view/app/dashboard/components/charts/server-specific-charts.tsx (1)
  • ServerSpcificCharts (10-37)
api/internal/features/dashboard/types.go (1)
api/internal/features/deploy/docker/init.go (1)
  • DockerService (22-26)
🔇 Additional comments (13)
api/Dockerfile.dev (1)

5-5: Justify the addition of openssh-client and verify necessity.

The addition of openssh-client to the development image lacks context. While the change itself is syntactically correct, it increases the image size and dependencies without documented justification.

Please clarify:

  1. Why is openssh-client needed in the development environment?
  2. Is it actively used during local development or CI/CD?
  3. Could this dependency be added conditionally or deferred to a separate build stage?

Additionally, the PR objectives describe adding Server/HTTP metrics to the dashboard, but this file change appears orthogonal to that scope. Please verify that this Dockerfile change is intentionally part of this metrics PR or if it's an unrelated dependency.

.gitignore (1)

70-70: LGTM!

Adding api/nixopus-api to the ignore list is appropriate and aligns with the PR objective of adding API-related metrics infrastructure.

view/lib/i18n/locales/es.json (1)

769-772: LGTM: dashboard.charts keys added

Translations are clear and consistent with other locales. No issues.

view/lib/i18n/locales/fr.json (1)

770-773: LGTM: dashboard.charts labels (FR)

Accurate and consistent. Nothing to fix.

view/lib/i18n/locales/kn.json (1)

766-769: LGTM: dashboard.charts labels (KN)

Additions align with other locales.

view/lib/i18n/locales/en.json (1)

786-789: LGTM: dashboard.charts labels (EN)

Keys and copy look good.

view/lib/i18n/locales/ml.json (1)

785-788: LGTM: dashboard.charts labels (ML)

Additions are consistent with other locales.

api/internal/features/dashboard/init.go (1)

82-83: Wire-up verified; all checks pass

✓ GetCaddyMetrics is a proper DashboardOperation constant (types.go:20) included in AllOperations (types.go:26)
(*DashboardMonitor).GetCaddyMetrics() exists (caddy_metrics.go:23) and is non-blocking—broadcasts metrics then returns
✓ Switch case at lines 82-83 is correctly wired

view/app/dashboard/components/system/system-stats.tsx (1)

8-9: LGTM! Clean integration of new components.

The new CPU cores and server metrics components are properly integrated with both their active and skeleton states, following the established pattern in the file.

Also applies to: 22-23, 33-34

view/app/dashboard/page.tsx (1)

12-12: Update import after fixing component name typo.

This import references the misspelled component name ServerSpcificCharts. Once the typo in server-specific-charts.tsx is corrected, update this import accordingly.

This comment is related to the typo identified in view/app/dashboard/components/charts/server-specific-charts.tsx line 10.

view/redux/types/monitor.ts (1)

57-97: LGTM! Well-structured type definitions.

The new type definitions are clear, properly typed, and follow TypeScript best practices. The optional fields are appropriately marked, and the interfaces are cohesive.

view/app/dashboard/components/system/cpu-cores.tsx (1)

14-46: LGTM! Clean implementation with proper null checks.

The component correctly handles edge cases (null/empty data) and provides a clear visualization of per-core CPU metrics.

api/internal/features/dashboard/system_stats.go (1)

231-251: LGTM! Clean disk growth rate calculation.

The disk growth rate calculation correctly handles the initial state, checks for zero elapsed time, and maintains the necessary state for tracking changes over time.

Comment on lines 74 to 71
const MonitoringSection = ({
systemStats,
containersData,
caddyMetrics,
caddyTimeSeries,
advancedTimeSeries,
t
}: {
systemStats: any;
containersData: any;
caddyMetrics: any;
caddyTimeSeries: any;
advancedTimeSeries: any;
t: any;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Replace any types with proper type definitions.

The MonitoringSection component uses any types for its props, which defeats the purpose of TypeScript's type safety. Consider defining a proper interface or using the existing types.

Apply this diff to add proper types:

+import { SystemStatsType } from '@/redux/types/monitor';
+import { ContainerData } from '@/redux/types/monitor';
+import { CaddyMetrics } from '@/redux/types/monitor';
+import { CaddyTimeSeriesData, ServerSpecificTimeSeriesData } from './hooks/use-monitor';
+
+interface MonitoringSectionProps {
+  systemStats: SystemStatsType | null;
+  containersData: ContainerData[];
+  caddyMetrics: CaddyMetrics | null;
+  caddyTimeSeries: CaddyTimeSeriesData;
+  advancedTimeSeries: ServerSpecificTimeSeriesData;
+  t: (key: string) => string;
+}
+
 const MonitoringSection = ({
   systemStats,
   containersData,
   caddyMetrics,
   caddyTimeSeries,
   advancedTimeSeries,
   t
-}: {
-  systemStats: any;
-  containersData: any;
-  caddyMetrics: any;
-  caddyTimeSeries: any;
-  advancedTimeSeries: any;
-  t: any;
-}) => {
+}: MonitoringSectionProps) => {
📝 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.

Suggested change
const MonitoringSection = ({
systemStats,
containersData,
caddyMetrics,
caddyTimeSeries,
advancedTimeSeries,
t
}: {
systemStats: any;
containersData: any;
caddyMetrics: any;
caddyTimeSeries: any;
advancedTimeSeries: any;
t: any;
import { SystemStatsType } from '@/redux/types/monitor';
import { ContainerData } from '@/redux/types/monitor';
import { CaddyMetrics } from '@/redux/types/monitor';
import { CaddyTimeSeriesData, ServerSpecificTimeSeriesData } from './hooks/use-monitor';
interface MonitoringSectionProps {
systemStats: SystemStatsType | null;
containersData: ContainerData[];
caddyMetrics: CaddyMetrics | null;
caddyTimeSeries: CaddyTimeSeriesData;
advancedTimeSeries: ServerSpecificTimeSeriesData;
t: (key: string) => string;
}
const MonitoringSection = ({
systemStats,
containersData,
caddyMetrics,
caddyTimeSeries,
advancedTimeSeries,
t
}: MonitoringSectionProps) => {
🤖 Prompt for AI Agents
In view/app/dashboard/page.tsx around lines 74 to 87, the MonitoringSection
props are typed as `any`; define a proper props interface (e.g.,
MonitoringSectionProps) that replaces `any` with concrete types (use existing
types from your domain if available: SystemStats, ContainerData[], CaddyMetrics,
TimeSeriesPoint[]/TimeSeries, AdvancedTimeSeries, and a translation function
type like TFunction from i18next), import those types or declare them locally,
then update the component signature to use the new interface and export/consume
it accordingly; ensure arrays and nullable/optional fields are typed correctly
and adjust call sites if needed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
view/app/dashboard/page.tsx (1)

126-136: Replace any types with proper type definitions.

The MonitoringSection component still uses any types for its props, defeating TypeScript's type safety. This was flagged in a previous review and remains unaddressed.

Apply this diff to add proper types:

+import { SystemStatsType } from '@/redux/types/monitor';
+import { ContainerData } from '@/redux/types/monitor';
+
+interface MonitoringSectionProps {
+  systemStats: SystemStatsType | null;
+  containersData: ContainerData[];
+  t: (key: string) => string;
+  layoutResetKey: number;
+}
+
 const MonitoringSection = ({
   systemStats,
   containersData,
   t,
   layoutResetKey
-}: {
-  systemStats: any;
-  containersData: any;
-  t: any;
-  layoutResetKey: number;
-}) => {
+}: MonitoringSectionProps) => {
🧹 Nitpick comments (3)
view/app/dashboard/components/system/load-average.tsx (1)

52-117: Consider removing unused components.

The LoadBar, LoadBarProps, and LoadBarSkeleton components are defined but never used in the render path. They appear to be leftover from a previous implementation.

Apply this diff to remove the unused code:

-interface LoadBarProps {
-  label: string;
-  value: number;
-}
-
-const LoadBar: React.FC<LoadBarProps> = ({ label, value }) => (
-  <div className="flex justify-between items-center">
-    <TypographyMuted className="text-xs sm:text-sm">{label}</TypographyMuted>
-    <div className="flex items-center">
-      <div className="w-20 sm:w-32 h-2 bg-gray-200 rounded-full mr-1 sm:mr-2">
-        <div
-          className={`h-2 rounded-full ${value > 80 ? 'bg-destructive' : 'bg-primary'}`}
-          style={{ width: `${Math.min(value * 25, 100)}%` }}
-        />
-      </div>
-      <TypographySmall className="text-xs sm:text-sm">{value.toFixed(2)}</TypographySmall>
-    </div>
-  </div>
-);
-
 export default LoadAverageCard;
 
 export function LoadAverageCardSkeleton() {
   const { t } = useTranslation();
 
   return (
     <Card className="overflow-hidden">
       <CardHeader className="pb-2">
         <CardTitle className="text-xs sm:text-sm font-medium flex items-center">
           <Activity className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2 text-muted-foreground" />
           <TypographySmall>{t('dashboard.load.title')}</TypographySmall>
         </CardTitle>
       </CardHeader>
       <CardContent>
         <div className="space-y-4">
           <div className="flex items-center justify-between">
             <TypographyMuted>{t('dashboard.load.labels.oneMin')}</TypographyMuted>
             <Skeleton className="h-4 w-12" />
           </div>
           <div className="flex items-center justify-between">
             <TypographyMuted>{t('dashboard.load.labels.fiveMin')}</TypographyMuted>
             <Skeleton className="h-4 w-12" />
           </div>
           <div className="flex items-center justify-between">
             <TypographyMuted>{t('dashboard.load.labels.fifteenMin')}</TypographyMuted>
             <Skeleton className="h-4 w-12" />
           </div>
         </div>
       </CardContent>
     </Card>
   );
 }
-
-interface LoadBarSkeletonProps {
-  label: string;
-}
-
-const LoadBarSkeleton: React.FC<LoadBarSkeletonProps> = ({ label }) => (
-  <div className="flex justify-between items-center">
-    <TypographyMuted className="text-xs sm:text-sm">{label}</TypographyMuted>
-    <div className="flex items-center">
-      <Skeleton className="w-20 sm:w-32 h-2 rounded-full mr-1 sm:mr-2" />
-      <Skeleton className="w-8 h-4" />
-    </div>
-  </div>
-);
view/app/dashboard/components/system/system-stats.tsx (1)

25-40: Consider memoizing the items array for performance.

The systemStatsItems array is recreated on every render when systemStats changes. Since each item contains JSX (React elements), this could cause unnecessary re-renders of the DraggableGrid component.

Apply this diff to memoize the items:

+import React, { useMemo } from 'react';
 import { SystemStatsType } from '@/redux/types/monitor';
 import SystemInfoCard, { SystemInfoCardSkeleton } from './system-info';
 import LoadAverageCard, { LoadAverageCardSkeleton } from './load-average';
 import MemoryUsageCard, { MemoryUsageCardSkeleton } from './memory-usage';
 import { DraggableGrid, DraggableItem } from '@/components/ui/draggable-grid';

 export interface SystemStatsProps {
   systemStats: SystemStatsType | null;
 }

 const SystemStats: React.FC<SystemStatsProps> = ({ systemStats }) => {
   if (!systemStats) {
     return (
       <div className="space-y-4">
         <SystemInfoCardSkeleton />
         <LoadAverageCardSkeleton />
         <MemoryUsageCardSkeleton />
       </div>
     );
   }

-  const systemStatsItems: DraggableItem[] = [
+  const systemStatsItems = useMemo<DraggableItem[]>(() => [
     {
       id: 'system-info',
       component: <SystemInfoCard systemStats={systemStats} />
     },
     {
       id: 'load-average',
       component: <LoadAverageCard systemStats={systemStats} />
     },
     {
       id: 'memory-usage',
       component: <MemoryUsageCard systemStats={systemStats} />
     }
-  ];
+  ], [systemStats]);

   return <DraggableGrid items={systemStatsItems} storageKey="system-stats-card-order" />;
 };
view/app/dashboard/page.tsx (1)

154-192: Memoize dashboardItems to prevent unnecessary re-renders.

The dashboardItems array is recreated on every render with inline JSX components, which can cause the DraggableGrid to re-mount unnecessarily and lose drag state.

Apply this diff to memoize the items:

+const dashboardItems: DraggableItem[] = React.useMemo(() => [
-const dashboardItems: DraggableItem[] = [
   {
     id: 'system-info',
     component: <SystemInfoCard systemStats={systemStats} />
   },
   {
     id: 'load-average',
     component: <LoadAverageCard systemStats={systemStats} />
   },
   {
     id: 'memory-usage',
     component: <MemoryUsageCard systemStats={systemStats} />
   },
   {
     id: 'disk-usage',
     component: <DiskUsageCard systemStats={systemStats} />
   },
   {
     id: 'containers',
     component: (
       <Card>
         <CardHeader className="flex flex-row items-center justify-between">
           <CardTitle className="text-xs sm:text-sm font-bold flex items-center">
             <Package className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2 text-muted-foreground" />
             <TypographySmall>{t('dashboard.containers.title')}</TypographySmall>
           </CardTitle>
           <Button variant="outline" size="sm" onClick={() => router.push('/containers')}>
             <ArrowRight className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2 text-muted-foreground" />
             {t('dashboard.containers.viewAll')}
           </Button>
         </CardHeader>
         <CardContent>
           <ContainersTable containersData={containersData} />
         </CardContent>
       </Card>
     ),
     className: 'md:col-span-2'
   }
-];
+], [systemStats, containersData, t, router]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6d5becb and 3d60502.

📒 Files selected for processing (8)
  • api/api/versions.json (1 hunks)
  • view/app/dashboard/components/system/load-average.tsx (1 hunks)
  • view/app/dashboard/components/system/memory-usage.tsx (1 hunks)
  • view/app/dashboard/components/system/system-info.tsx (1 hunks)
  • view/app/dashboard/components/system/system-stats.tsx (2 hunks)
  • view/app/dashboard/page.tsx (5 hunks)
  • view/components/ui/draggable-grid.tsx (1 hunks)
  • view/package.json (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
view/app/dashboard/components/system/load-average.tsx (1)
view/redux/types/monitor.ts (1)
  • SystemStatsType (59-66)
view/app/dashboard/components/system/memory-usage.tsx (2)
view/redux/types/monitor.ts (1)
  • SystemStatsType (59-66)
view/hooks/use-translation.ts (1)
  • useTranslation (18-74)
view/app/dashboard/components/system/system-info.tsx (1)
view/redux/types/monitor.ts (1)
  • SystemStatsType (59-66)
view/app/dashboard/components/system/system-stats.tsx (1)
view/components/ui/draggable-grid.tsx (2)
  • DraggableItem (26-30)
  • DraggableGrid (93-235)
view/components/ui/draggable-grid.tsx (1)
view/lib/utils.ts (1)
  • cn (4-6)
view/app/dashboard/page.tsx (2)
view/app/dashboard/components/smtp-banner.tsx (1)
  • SMTPBanner (13-51)
view/components/ui/draggable-grid.tsx (2)
  • DraggableItem (26-30)
  • DraggableGrid (93-235)
🔇 Additional comments (9)
api/api/versions.json (1)

6-6: LGTM!

The timestamp update reflects the current deployment and is consistent with standard versioning practices.

view/app/dashboard/components/system/load-average.tsx (1)

18-20: LGTM!

The null-safety guard is correctly implemented and consistent with the skeleton rendering pattern used across other system cards.

view/app/dashboard/components/system/memory-usage.tsx (1)

20-22: LGTM!

The null-safety guard is correctly implemented and follows the same pattern as the other system cards, ensuring consistent skeleton rendering behavior.

view/app/dashboard/components/system/system-info.tsx (1)

18-20: LGTM!

The null-safety guard is correctly implemented and maintains consistency with the other system card components.

view/package.json (1)

15-17: LGTM!

The @dnd-kit dependencies are correctly added to support the new drag-and-drop functionality. The versions are current and compatible with React 19.

view/components/ui/draggable-grid.tsx (1)

195-205: LGTM!

The SSR-safe fallback rendering before mount prevents hydration mismatches while showing the initial layout correctly.

view/app/dashboard/page.tsx (3)

40-52: LGTM!

The one-time drag hint with localStorage persistence provides good UX for onboarding users to the new drag-and-drop functionality.


54-57: LGTM!

The reset layout mechanism correctly clears localStorage and triggers a grid refresh via the resetKey increment pattern.


79-111: LGTM!

The Reset Layout button and drag hint banner provide clear user controls and guidance for the new draggable dashboard feature.

Comment on lines +122 to +152
useEffect(() => {
if (!mounted) return;

if (typeof window !== 'undefined' && storageKey) {
const savedOrder = localStorage.getItem(storageKey);
if (savedOrder) {
try {
const orderIds = JSON.parse(savedOrder) as string[];
const reordered = orderIds
.map(id => items.find(item => item.id === id))
.filter(Boolean) as DraggableItem[];

const newItems = items.filter(
item => !orderIds.includes(item.id)
);

if (reordered.length > 0) {
setOrderedItems([...reordered, ...newItems]);
} else {
setOrderedItems(items);
}
} catch (e) {
console.error('Failed to load saved order:', e);
setOrderedItems(items);
}
} else {
// if no saved order, use default
setOrderedItems(items);
}
}
}, [mounted, storageKey, resetKey, items]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix dependency array to prevent performance issues.

The items dependency in the array at line 152 will cause this effect to re-run every time the parent component re-renders with a new items array reference, even if the actual items haven't changed. This can lead to constant localStorage reloads and poor performance.

Consider using a stable dependency like items.map(i => i.id).join(',') or accept that items only sync on mount + resetKey changes:

   useEffect(() => {
     if (!mounted) return;

     if (typeof window !== 'undefined' && storageKey) {
       const savedOrder = localStorage.getItem(storageKey);
       if (savedOrder) {
         try {
           const orderIds = JSON.parse(savedOrder) as string[];
           const reordered = orderIds
             .map(id => items.find(item => item.id === id))
             .filter(Boolean) as DraggableItem[];

           const newItems = items.filter(
             item => !orderIds.includes(item.id)
           );

           if (reordered.length > 0) {
             setOrderedItems([...reordered, ...newItems]);
           } else {
             setOrderedItems(items);
           }
         } catch (e) {
           console.error('Failed to load saved order:', e);
           setOrderedItems(items);
         }
       } else {
         // if no saved order, use default
         setOrderedItems(items);
       }
     }
-  }, [mounted, storageKey, resetKey, items]);
+  }, [mounted, storageKey, resetKey]);

Then rely on the second effect (lines 155-164) to handle external item changes. Alternatively, add a separate prop like itemsKey: string that changes only when items structurally change.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In view/components/ui/draggable-grid.tsx around lines 122 to 152 the useEffect
depends on the full items array which will retrigger on every parent re-render
when items has a new reference; replace the items dependency with a stable
identifier (for example items.map(i => i.id).join(',') or a dedicated itemsKey
prop that only changes when the item set actually changes) or remove items from
the dependency array and rely on mounted/resetKey to control syncing; update the
dependency array accordingly so the effect only runs on mount, resetKey changes,
or when the stable items identifier changes.

Comment on lines +155 to +164
useEffect(() => {
if (!mounted) return;

if (orderedItems.length !== items.length) {
const savedOrder = localStorage.getItem(storageKey);
if (!savedOrder) {
setOrderedItems(items);
}
}
}, [items.length]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add missing dependencies to effect.

The effect at lines 155-164 is missing storageKey and mounted in its dependency array, which could cause it to access stale values.

Apply this diff:

   // Update items when they change externally (but keep order if saved)
   useEffect(() => {
     if (!mounted) return;

     if (orderedItems.length !== items.length) {
       const savedOrder = localStorage.getItem(storageKey);
       if (!savedOrder) {
         setOrderedItems(items);
       }
     }
-  }, [items.length]);
+  }, [items.length, mounted, storageKey, orderedItems.length]);
🤖 Prompt for AI Agents
In view/components/ui/draggable-grid.tsx around lines 155 to 164, the useEffect
currently depends only on items.length but also reads mounted,
orderedItems.length and storageKey; update the effect dependency array to
include mounted, storageKey and orderedItems.length (e.g. [items.length,
mounted, storageKey, orderedItems.length]) so the effect reruns when those
values change and avoids reading stale values; ensure no other external values
used inside the effect are left out of the dependency list.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (4)
api/internal/features/dashboard/types.go (1)

45-56: Timestamp should be numeric epoch (frontend expects number).

Current type is time.Time; the UI expects a number (Unix millis). This mismatch breaks parsing/serialization. Switch to int64 and populate with UnixMilli.

 type SystemStats struct {
   OSType        string      `json:"os_type"`
   Hostname      string      `json:"hostname"`
   CPUInfo       string      `json:"cpu_info"`
   CPUCores      int         `json:"cpu_cores"`
   Memory        MemoryStats `json:"memory"`
   Load          LoadStats   `json:"load"`
   Disk          DiskStats   `json:"disk"`
   KernelVersion string      `json:"kernel_version"`
   Architecture  string      `json:"architecture"`
-  Timestamp     time.Time   `json:"timestamp"`
+  Timestamp     int64       `json:"timestamp"` // Unix milliseconds
 }

Follow-up: ensure all producers set Timestamp via time.Now().UnixMilli() (or sourceTime.UnixMilli()).

#!/bin/bash
# Find SystemStats builders to update Timestamp assignment
rg -n -C3 'SystemStats\\s*\\{' --glob '!**/vendor/**'
rg -n 'Timestamp\\s*:' --glob '!**/vendor/**'
view/app/dashboard/page.tsx (1)

126-136: Stop using any for MonitoringSection props; add a proper props interface.

Use real types for systemStats, containersData, and t. Also include layoutResetKey in the interface. This mirrors prior feedback.

+import type { SystemStatsType, ContainerData } from '@/redux/types/monitor';
+import type { useTranslation } from '@/hooks/use-translation';
@@
-const MonitoringSection = ({
-  systemStats,
-  containersData,
-  t,
-  layoutResetKey
-}: {
-  systemStats: any;
-  containersData: any;
-  t: any;
-  layoutResetKey: number;
-}) => {
+interface MonitoringSectionProps {
+  systemStats: SystemStatsType | null;
+  containersData: ContainerData[];
+  t: ReturnType<typeof useTranslation>['t'];
+  layoutResetKey: number;
+}
+
+const MonitoringSection = ({
+  systemStats,
+  containersData,
+  t,
+  layoutResetKey
+}: MonitoringSectionProps) => {
view/components/ui/draggable-grid.tsx (2)

122-153: Avoid re-running the load-from-localStorage effect on every new items reference.

Remove items from deps or gate by a stable key; this was flagged earlier.

-    }, [mounted, storageKey, resetKey, items]);
+    }, [mounted, storageKey, resetKey]);

Optionally add a prop itemsKey (e.g., items.map(i => i.id).join(',')) if you need to re-sync when the item set changes structurally.


155-165: Complete the dependency list for the second effect.

Include mounted, storageKey, and orderedItems.length to avoid stale reads.

-    }, [items.length]);
+    }, [items.length, mounted, storageKey, orderedItems.length]);
🧹 Nitpick comments (21)
api/internal/features/dashboard/system_stats.go (6)

35-41: Don't hard‑fail on OS type; degrade gracefully.

If uname -s fails, we currently abort the whole stats collection. Prefer continuing with partial stats and setting OSType to "unknown" (plus logging) to keep the dashboard responsive.

-  if err != nil {
-    m.BroadcastError(err.Error(), GetSystemStats)
-    return
-  }
+  if err != nil {
+    m.log.Log(logger.Error, "Failed to detect OS type", err.Error())
+    osType = "unknown"
+  }

50-63: Avoid shelling out for host metadata when possible.

Use gopsutil/runtime for hostname, kernel, and arch to reduce SSH calls and latency.

+  // Prefer library calls; fall back to command if needed
+  if hi, err := host.Info(); err == nil && hi.Hostname != "" {
+    stats.Hostname = strings.TrimSpace(hi.Hostname)
+  } else if hostname, err := m.getCommandOutput("hostname"); err == nil {
+    stats.Hostname = strings.TrimSpace(hostname)
+  }
- if kernelVersion, err := m.getCommandOutput("uname -r"); err == nil {
-   stats.KernelVersion = strings.TrimSpace(kernelVersion)
- }
+  if kv, err := host.KernelVersion(); err == nil && kv != "" {
+    stats.KernelVersion = strings.TrimSpace(kv)
+  } else if kernelVersion, err := m.getCommandOutput("uname -r"); err == nil {
+    stats.KernelVersion = strings.TrimSpace(kernelVersion)
+  }
- if architecture, err := m.getCommandOutput("uname -m"); err == nil {
-   stats.Architecture = strings.TrimSpace(architecture)
- }
+  if stats.Architecture == "" {
+    stats.Architecture = strings.TrimSpace(runtime.GOARCH)
+  }

65-69: Idiomatic uptime calculation.

Safer and clearer as Duration math rather than uint64 nanoseconds conversion.

-    uptime = time.Duration(hostInfo.Uptime * uint64(time.Second)).String()
+    uptime = (time.Duration(hostInfo.Uptime) * time.Second).String()

71-79: Prefer gopsutil for load averages; keep parsing as fallback.

Avoid parsing uptime output; it varies by distro/shell. Use load.Avg() first, then fallback to parsing.

+  // Try library first
+  if avg, err := load.Avg(); err == nil {
+    stats.Load.OneMin = avg.Load1
+    stats.Load.FiveMin = avg.Load5
+    stats.Load.FifteenMin = avg.Load15
+  } else if loadAvg, err := m.getCommandOutput("uptime"); err == nil {
-  if loadAvg, err := m.getCommandOutput("uptime"); err == nil {
     loadAvgStr := strings.TrimSpace(loadAvg)
     stats.Load = parseLoadAverage(loadAvgStr)
   }
   // Set uptime after parsing load averages
   stats.Load.Uptime = uptime

Add import:

+ "github.com/shirou/gopsutil/v3/load"

86-91: Redundant 0-check for CPU cores.

stats.CPUCores is zero-initialized; just assign the count.

- if stats.CPUCores == 0 {
-   if coreCount, err := cpu.Counts(true); err == nil {
-     stats.CPUCores = coreCount
-   }
- }
+ if coreCount, err := cpu.Counts(true); err == nil {
+   stats.CPUCores = coreCount
+ }

158-178: Consider command timeouts to avoid hangs.

session.Run(cmd) can hang on network hiccups. If your SSH client connection isn't deadline-protected, add timeouts (connection-level or by using session.Start + Wait with a timer) to bound execution.

view/lib/i18n/locales/es.json (1)

808-822: System labels OK; fix memory unit mismatch and layout nesting.

  • memory uses MB here while other locales use GB; standardize to GB to keep UI consistent.
  • Ensure layout is a top-level key (not nested under navigation) like in en/fr/ml.
-      "used": "{value} MB usado",
-      "total": "{value} MB total",
+      "used": "{value} GB usado",
+      "total": "{value} GB total",
view/lib/i18n/locales/fr.json (1)

809-823: System labels look good; clean up unrelated strings.

Elsewhere in this file, “Monitoreo” and “Partes de CPU” appear (Spanish). Replace with proper French (“Surveillance”, “Parts CPU”) to avoid mixed-language UI.

view/components/ui/bar-chart-component.tsx (1)

73-75: Prefer stable keys over index.

Use a deterministic key (e.g., entry.name) to avoid unnecessary re-renders on data reordering.

-          {data.map((entry, index) => (
-            <Cell key={`cell-${index}`} fill={entry.fill} />
+          {data.map((entry) => (
+            <Cell key={entry.name} fill={entry.fill} />
           ))}
view/components/ui/doughnut-chart-component.tsx (2)

5-5: Make the chart responsive (avoid fixed 200×200).

Use ResponsiveContainer so the chart adapts to its wrapper size.

-import { PieChart, Pie, Cell, Label } from 'recharts';
+import { PieChart, Pie, Cell, Label, ResponsiveContainer } from 'recharts';
@@
-      <PieChart width={200} height={200}>
+      <ResponsiveContainer width="100%" height="100%">
+        <PieChart>
@@
-      </PieChart>
+        </PieChart>
+      </ResponsiveContainer>

Also applies to: 35-36, 79-80


47-49: Use stable keys for Cells.

Prefer entry.name (assuming unique) over array index.

-          {data.map((entry, index) => (
-            <Cell key={`cell-${index}`} fill={entry.fill} />
+          {data.map((entry) => (
+            <Cell key={entry.name} fill={entry.fill} />
           ))}
view/app/dashboard/components/system/memory-usage.tsx (2)

28-29: Clamp free memory to non-negative.

Minor guard against rounding or sampling inconsistencies.

-  const freeMemory = memory.total - memory.used;
+  const freeMemory = Math.max(0, memory.total - memory.used);

31-42: Align chartConfig keys with data names; localize labels; prefer theme tokens over hex.

  • chartConfig keys often drive tooltip/legend mapping; align them with data item names.
  • Localize “Used”, “Free”, “Total”.
  • Consider CSS tokens (e.g., hsl(var(--chart-1))) for dark/light theme consistency.
-  const chartData: DoughnutChartDataItem[] = [
+  const chartData: DoughnutChartDataItem[] = [
     {
-      name: 'Used',
+      name: t('dashboard.common.used'),
       value: memory.used,
-      fill: '#3b82f6'
+      fill: 'hsl(var(--chart-1))'
     },
     {
-      name: 'Free',
+      name: t('dashboard.common.free'),
       value: freeMemory,
-      fill: '#10b981'
+      fill: 'hsl(var(--chart-2))'
     }
   ];
@@
-  const chartConfig = {
-    used: { label: 'Used Memory', color: '#3b82f6' },
-    free: { label: 'Free Memory', color: '#10b981' }
-  };
+  const chartConfig = {
+    [t('dashboard.common.used')]: { label: t('dashboard.memory.usedLabel'), color: 'hsl(var(--chart-1))' },
+    [t('dashboard.common.free')]: { label: t('dashboard.memory.freeLabel'), color: 'hsl(var(--chart-2))' }
+  };
@@
-              centerLabel={{
-                value: `${memory.percentage.toFixed(1)}%`,
-                subLabel: 'Used'
-              }}
+              centerLabel={{
+                value: `${memory.percentage.toFixed(1)}%`,
+                subLabel: t('dashboard.common.used')
+              }}
@@
-                <TypographyMuted>
-                  Used: {formatGB(memory.used)} GB
+                <TypographyMuted>
+                  {t('dashboard.common.used')}: {formatGB(memory.used)} GB
                 </TypographyMuted>
@@
-                <TypographyMuted>
-                  Free: {formatGB(freeMemory)} GB
+                <TypographyMuted>
+                  {t('dashboard.common.free')}: {formatGB(freeMemory)} GB
                 </TypographyMuted>
@@
-            <TypographyMuted className="text-xs text-center">
-              Total: {formatGB(memory.total)} GB
+            <TypographyMuted className="text-xs text-center">
+              {t('dashboard.common.total')}: {formatGB(memory.total)} GB
             </TypographyMuted>

Please also confirm the memory unit from the API is GiB; if it’s bytes, we should convert before formatting.

Also applies to: 44-53, 71-73, 85-94, 98-100

view/app/dashboard/components/system/load-average.tsx (1)

44-57: Align config keys with data names and localize labels; prefer theme tokens.

  • Keep chartConfig keys consistent with displayed series names to ensure correct legend/tooltip mapping.
  • Localize axis and summary labels.
  • Use theme tokens for colors for better theming.
-  const chartConfig = {
-    oneMin: { label: '1 min', color: '#3b82f6' },
-    fiveMin: { label: '5 min', color: '#10b981' },
-    fifteenMin: { label: '15 min', color: '#f59e0b' }
-  };
+  const chartConfig = {
+    '1 min': { label: t('dashboard.load.oneMin'), color: 'hsl(var(--chart-1))' },
+    '5 min': { label: t('dashboard.load.fiveMin'), color: 'hsl(var(--chart-2))' },
+    '15 min': { label: t('dashboard.load.fifteenMin'), color: 'hsl(var(--chart-3))' }
+  };
@@
-              yAxisLabel="Load"
-              xAxisLabel="Time Period"
+              yAxisLabel={t('dashboard.load.axis.load')}
+              xAxisLabel={t('dashboard.load.axis.timePeriod')}
@@
-                <div className="h-2 w-2 rounded-full" style={{ backgroundColor: '#3b82f6' }} />
-                <TypographyMuted className="text-xs">1 min</TypographyMuted>
+                <div className="h-2 w-2 rounded-full" style={{ backgroundColor: 'hsl(var(--chart-1))' }} />
+                <TypographyMuted className="text-xs">{t('dashboard.load.oneMin')}</TypographyMuted>
@@
-                <div className="h-2 w-2 rounded-full" style={{ backgroundColor: '#10b981' }} />
-                <TypographyMuted className="text-xs">5 min</TypographyMuted>
+                <div className="h-2 w-2 rounded-full" style={{ backgroundColor: 'hsl(var(--chart-2))' }} />
+                <TypographyMuted className="text-xs">{t('dashboard.load.fiveMin')}</TypographyMuted>
@@
-                <div className="h-2 w-2 rounded-full" style={{ backgroundColor: '#f59e0b' }} />
-                <TypographyMuted className="text-xs">15 min</TypographyMuted>
+                <div className="h-2 w-2 rounded-full" style={{ backgroundColor: 'hsl(var(--chart-3))' }} />
+                <TypographyMuted className="text-xs">{t('dashboard.load.fifteenMin')}</TypographyMuted>

If ChartTooltipContent relies on config keys to format labels, this alignment prevents mismatches.

Also applies to: 74-76, 81-101, 128-140

view/app/dashboard/page.tsx (1)

154-193: Memoize dashboardItems to avoid unstable items prop causing extra work.

Recreate the array only when inputs change; this cooperates with DraggableGrid effects and prevents unnecessary localStorage reads.

-  const dashboardItems: DraggableItem[] = [
+  const dashboardItems = React.useMemo<DraggableItem[]>(() => [
     {
       id: 'system-info',
       component: <SystemInfoCard systemStats={systemStats} />,
       className: 'md:col-span-2'
     },
     {
       id: 'load-average',
       component: <LoadAverageCard systemStats={systemStats} />
     },
     {
       id: 'memory-usage',
       component: <MemoryUsageCard systemStats={systemStats} />
     },
     {
       id: 'disk-usage',
       component: <DiskUsageCard systemStats={systemStats} />
     },
     {
       id: 'containers',
       component: (
         <Card>
@@
         </Card>
       ),
       className: 'md:col-span-2'
     }
-  ];
+  ], [systemStats, containersData, t]);
view/app/dashboard/components/system/system-info.tsx (3)

22-26: Localize literals and units; add safer fallbacks.

Replace hardcoded “N/A”, “GB”, and “cores” with translations; ensure uptime falls back when absent.

-  const memoryDisplay = `${memory.used.toFixed(1)} / ${memory.total.toFixed(1)} GB (${memory.percentage.toFixed(1)}%)`;
+  const { t } = useTranslation(); // already present above
+  const NA = t('common.na');
+  const GB = t('units.gb'); // add to locales if missing
+  const memoryDisplay =
+    `${memory.used.toFixed(1)} / ${memory.total.toFixed(1)} ${GB} (${memory.percentage.toFixed(1)}%)`;
@@
-      value: os_type || 'N/A'
+      value: os_type || NA
@@
-      value: hostname || 'N/A'
+      value: hostname || NA
@@
-      value: cpu_info || 'N/A'
+      value: cpu_info || NA
@@
-      value: cpu_cores > 0 ? `${cpu_cores} cores` : 'N/A'
+      value: cpu_cores > 0 ? t('dashboard.system.values.cpuCores', { count: String(cpu_cores) }) : NA
@@
-      value: kernel_version || 'N/A'
+      value: kernel_version || NA
@@
-      value: load.uptime?.replaceAll(/([hms])(\d)/g, '$1 $2')
+      value: load?.uptime
+        ? load.uptime.replaceAll(/([hms])(\d)/g, '$1 $2')
+        : NA
@@
-      value: architecture || 'N/A'
+      value: architecture || NA

Add these new keys if needed:

  • common.na
  • units.gb
  • dashboard.system.values.cpuCores = "{count} cores" (or localized equivalent)

Also applies to: 29-67


3-5: Remove unused imports.

Cpu isn’t used; drop it to reduce bundle size.

-import { Server, Cpu, HardDrive, Activity, Terminal, Box, CpuIcon, ScreenShare, ServerCog } from 'lucide-react';
+import { Server, HardDrive, Activity, Terminal, Box, CpuIcon, ScreenShare, ServerCog } from 'lucide-react';

80-83: Prefer a stable key over array index.

Use label (string) as key to avoid potential reconciliation issues.

-          {systemInfoItems.map((item, index) => (
-            <div key={index} className="flex items-start gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors">
+          {systemInfoItems.map((item) => (
+            <div key={String(item.label)} className="flex items-start gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors">
view/components/ui/draggable-grid.tsx (3)

41-47: Remove unused isDragging prop from SortableItem.

It’s never used; rely on useSortable().isDragging.

-function SortableItem({
-    item,
-    isDragging
-}: {
-    item: DraggableItem;
-    isDragging?: boolean;
-}) {
+function SortableItem({ item }: { item: DraggableItem }) {

73-82: Improve drag handle accessibility and focus affordance.

Expose the handle on keyboard focus and add an accessible label.

-            <div
+            <div
                 {...listeners}
                 {...attributes}
-                className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-3 opacity-0 group-hover:opacity-100 transition-opacity cursor-grab active:cursor-grabbing z-10 touch-none"
-                title="Drag to reorder"
+                className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-3 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity cursor-grab active:cursor-grabbing z-10 touch-none"
+                aria-label="Drag to reorder"
+                title="Drag to reorder"
             >

If you prefer full semantics, render a <button type="button"> for the handle and spread attributes/listeners onto it.


155-165: Keep order but refresh item content when props change with same length.

When items.length is unchanged but component/className updates, orderedItems stays stale. Merge by id to refresh content while preserving order.

-    // Update items when they change externally (but keep order if saved)
-    useEffect(() => {
+    // Update items when they change externally (refresh content but keep saved order)
+    useEffect(() => {
         if (!mounted) return;
 
-        if (orderedItems.length !== items.length) {
+        if (orderedItems.length !== items.length) {
             const savedOrder = localStorage.getItem(storageKey);
             if (!savedOrder) {
                 setOrderedItems(items);
             }
+        } else {
+            const incomingById = new Map(items.map(i => [i.id, i]));
+            setOrderedItems(prev =>
+                prev.map(i => incomingById.get(i.id) ?? i)
+            );
         }
-    }, [items.length]);
+    }, [items.length, mounted, storageKey, orderedItems.length, items]);

Pair this with memoized items from the parent (see page.tsx) to avoid unnecessary churn.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d60502 and 39d9b03.

📒 Files selected for processing (17)
  • api/api/versions.json (1 hunks)
  • api/internal/features/dashboard/system_stats.go (1 hunks)
  • api/internal/features/dashboard/types.go (1 hunks)
  • view/app/dashboard/components/system/disk-usage.tsx (2 hunks)
  • view/app/dashboard/components/system/load-average.tsx (1 hunks)
  • view/app/dashboard/components/system/memory-usage.tsx (2 hunks)
  • view/app/dashboard/components/system/system-info.tsx (2 hunks)
  • view/app/dashboard/page.tsx (5 hunks)
  • view/components/ui/bar-chart-component.tsx (1 hunks)
  • view/components/ui/doughnut-chart-component.tsx (1 hunks)
  • view/components/ui/draggable-grid.tsx (1 hunks)
  • view/lib/i18n/locales/en.json (1 hunks)
  • view/lib/i18n/locales/es.json (1 hunks)
  • view/lib/i18n/locales/fr.json (1 hunks)
  • view/lib/i18n/locales/kn.json (1 hunks)
  • view/lib/i18n/locales/ml.json (1 hunks)
  • view/redux/types/monitor.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • view/app/dashboard/components/system/disk-usage.tsx
🧰 Additional context used
🧬 Code graph analysis (8)
view/app/dashboard/components/system/memory-usage.tsx (5)
view/redux/types/monitor.ts (1)
  • SystemStatsType (59-70)
view/hooks/use-translation.ts (1)
  • useTranslation (18-74)
view/components/ui/doughnut-chart-component.tsx (2)
  • DoughnutChartDataItem (7-11)
  • DoughnutChartComponent (25-82)
view/components/ui/typography.tsx (2)
  • TypographySmall (99-112)
  • TypographyMuted (114-122)
view/components/ui/skeleton.tsx (1)
  • Skeleton (13-13)
view/app/dashboard/components/system/load-average.tsx (3)
view/redux/types/monitor.ts (1)
  • SystemStatsType (59-70)
view/components/ui/bar-chart-component.tsx (2)
  • BarChartDataItem (7-11)
  • BarChartComponent (22-80)
view/components/ui/typography.tsx (2)
  • TypographySmall (99-112)
  • TypographyMuted (114-122)
api/internal/features/dashboard/types.go (1)
view/redux/types/monitor.ts (3)
  • MemoryStats (27-32)
  • LoadStats (34-39)
  • DiskStats (50-57)
view/redux/types/monitor.ts (1)
api/internal/features/dashboard/types.go (3)
  • MemoryStats (58-63)
  • LoadStats (65-70)
  • DiskStats (81-88)
view/app/dashboard/page.tsx (1)
view/components/ui/draggable-grid.tsx (2)
  • DraggableItem (26-30)
  • DraggableGrid (93-235)
view/components/ui/bar-chart-component.tsx (1)
view/components/ui/chart.tsx (3)
  • ChartContainer (316-316)
  • ChartTooltip (317-317)
  • ChartTooltipContent (318-318)
view/components/ui/draggable-grid.tsx (1)
view/lib/utils.ts (1)
  • cn (4-6)
view/app/dashboard/components/system/system-info.tsx (3)
view/redux/types/monitor.ts (1)
  • SystemStatsType (59-70)
view/hooks/use-translation.ts (1)
  • useTranslation (18-74)
view/components/ui/typography.tsx (2)
  • TypographySmall (99-112)
  • TypographyMuted (114-122)
🔇 Additional comments (7)
api/api/versions.json (1)

6-6: LGTM, just confirm release timestamp source.

Looks fine. Ensure this release_date is generated from your build/release process and is monotonic across releases to avoid update-check regressions.

view/lib/i18n/locales/ml.json (1)

824-838: System labels added correctly.

Keys align with other locales. No issues spotted here.

view/lib/i18n/locales/kn.json (1)

805-819: System labels addition looks good.

Matches the expected schema.

view/app/dashboard/components/system/memory-usage.tsx (1)

21-23: Null-state handling LGTM.

Early return to skeleton is clean and prevents crashes.

view/app/dashboard/components/system/load-average.tsx (1)

19-21: Null-state handling LGTM.

Early skeleton return is correct and consistent with other cards.

view/components/ui/bar-chart-component.tsx (1)

35-37: No issues found—ChartContainer correctly implements ResponsiveContainer.

The verification confirms that ChartContainer properly wraps children with RechartsPrimitive.ResponsiveContainer (line 62 of view/components/ui/chart.tsx), ensuring the chart will resize predictably. The BarChart component will handle responsiveness as expected.

view/app/dashboard/components/system/system-info.tsx (1)

24-26: No action needed—memory units are correct.

The backend converts memory from bytes to GB before sending (float64(memInfo.Total) / bytesInGB where bytesInGB = 1024 * 1024 * 1024), so the frontend values are already in GB. The code at line 25 is correct.

Likely an incorrect or invalid review comment.

Comment on lines 79 to 87
<Button
variant="outline"
size="sm"
onClick={handleResetLayout}
className="shrink-0"
>
<RefreshCw className="mr-2 h-4 w-4" />
Reset Layout
</Button>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Externalize hardcoded UI text to i18n.

“Reset Layout”, “Customize Your Dashboard”, the hint paragraph, and “Got it” should use t(...) like the rest of the page.

-          <Button
+          <Button
             variant="outline"
             size="sm"
             onClick={handleResetLayout}
             className="shrink-0"
+            aria-label={t('dashboard.actions.resetLayout')}
           >
             <RefreshCw className="mr-2 h-4 w-4" />
-            Reset Layout
+            {t('dashboard.actions.resetLayout')}
           </Button>
@@
-                <p className="text-sm font-medium text-foreground">Customize Your Dashboard</p>
+                <p className="text-sm font-medium text-foreground">
+                  {t('dashboard.dragHint.title')}
+                </p>
                 <p className="text-xs text-muted-foreground mt-1">
-                  Hover over any card to see the drag handle on the left. Click and drag to rearrange cards in your preferred order. Your layout will be saved automatically.
+                  {t('dashboard.dragHint.body')}
                 </p>
@@
-            >
-              Got it
+            >
+              {t('common.gotIt')}
             </Button>

Ensure keys exist (or add them) in locales:

  • dashboard.actions.resetLayout
  • dashboard.dragHint.title
  • dashboard.dragHint.body
  • common.gotIt

Also applies to: 90-111

Comment on lines +825 to +839
"lastUpdated": "Last Updated",
"hostname": "Hostname",
"kernelVersion": "Kernel Version",
"architecture": "Architecture",
"memory": "Memory",
"labels": {
"operatingSystem": "Operating System",
"hostname": "Hostname",
"cpu": "CPU",
"cpuCores": "CPU Cores",
"memory": "Memory",
"kernelVersion": "Kernel Version",
"uptime": "Uptime",
"architecture": "Architecture"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

New system labels OK; run an i18n sanity check.

Keys look correct. To prevent runtime issues, please verify placeholders and structure across locales (e.g., non-English placeholder names, misplaced 'layout', unit inconsistencies).


🏁 Script executed:

#!/bin/bash
# i18n sanity checks: placeholder tokens, layout position, memory units consistency.

python - << 'PY'
import json, glob, re, sys, os
files = sorted(glob.glob("view/lib/i18n/locales/*.json"))
allowed_placeholders = {"name","count","number","current","total"}
issues = []
# 1) Placeholder tokens should be ASCII and from allowed set
def walk(obj, path=""):
    if isinstance(obj, dict):
        for k,v in obj.items():
            walk(v, f"{path}.{k}" if path else k)
    elif isinstance(obj, list):
        for i,v in enumerate(obj):
            walk(v, f"{path}[{i}]")
    elif isinstance(obj, str):
        for m in re.finditer(r"\{([^}]+)\}", obj):
            token = m.group(1)
            if not token.isascii():
                issues.append(("non_ascii_placeholder", path, token, obj))
            elif token not in allowed_placeholders:
                issues.append(("unknown_placeholder", path, token, obj))

# 2) layout must be top-level, not nested inside navigation
layout_issues = []
# 3) Memory unit consistency
mem_units = {}
def unit_from(s):
    # capture GB/MB/Go/Mo tokens
    m = re.search(r"\b(GO|Go|GB|MB|Mo)\b", s)
    return m.group(0) if m else None

for f in files:
    with open(f, "r", encoding="utf-8") as fh:
        data = json.load(fh)
    walk(data)
    root_has_layout = "layout" in data
    nav_has_layout = isinstance(data.get("navigation"), dict) and "layout" in data["navigation"]
    if not root_has_layout or nav_has_layout:
        layout_issues.append((os.path.basename(f), root_has_layout, nav_has_layout))
    # memory units
    try:
        used = data["dashboard"]["memory"]["used"]
        total = data["dashboard"]["memory"]["total"]
        mem_units[os.path.basename(f)] = (unit_from(used), unit_from(total))
    except Exception:
        pass

if issues:
    print("Placeholder issues:")
    for t,p,token,val in issues:
        print(f" - {t}: {p} -> '{token}' in '{val}'")
    print()
if layout_issues:
    print("Layout key issues (expect layout at root, not under navigation):")
    for name,root_ok,nav_has in layout_issues:
        print(f" - {name}: root_layout={root_ok}, navigation.layout={nav_has}")
    print()
# Unit consistency report
if mem_units:
    print("Memory units by locale (used,total):")
    for k,v in mem_units.items():
        print(f" - {k}: {v}")
    units = set(u for pair in mem_units.values() for u in pair if u)
    if len(units) > 1:
        print(f"\nInconsistent units detected across locales: {sorted(units)}")
        print("Recommend standardizing to GB equivalents for dashboard.memory.*")
else:
    print("No memory keys found.")
PY

Length of output: 9256


Fix critical i18n issues in locale files before merge.

Verification revealed three breaking issues:

  1. Non-ASCII placeholders in Malayalam (ml.json): Strings use localized placeholder names ({എണ്ണുക}, {പേര്}, {മൂല്യം}) instead of ASCII equivalents. Runtime interpolation will fail—replace with ASCII tokens (e.g., {count}, {name}, {value}).

  2. Misplaced layout key in Spanish (es.json): The layout key is nested under navigation instead of at root level, causing config/parsing mismatches.

  3. Inconsistent memory units: Spanish uses MB; English, Kannada, and Malayalam use GB; French uses Go. Standardize to GB across all locales to prevent user confusion and data interpretation errors.

🤖 Prompt for AI Agents
In view/lib/i18n/locales/en.json around lines 825 to 839, the "memory"
label/unit must be standardized to "GB" (not "MB" or "Go") and ensure the nested
labels keys are consistent with other locales; update the "memory" value to use
"GB" and keep the label keys unchanged. Also, fix the other reported locale
issues: in view/lib/i18n/locales/ml.json replace any non-ASCII placeholder names
(e.g., {എണ്ണുക}, {പേര്}, {മൂല്യം}) with ASCII tokens like {count}, {name},
{value}; and in view/lib/i18n/locales/es.json move the misplaced "layout" key
out from under "navigation" to the root of the JSON so parsing/config matches
expected structure.

Comment on lines 59 to 70
export interface SystemStatsType {
os_type: string;
hostname: string;
cpu_info: string;
cpu_cores: number;
memory: MemoryStats;
load: LoadStats;
disk: DiskStats;
kernel_version: string;
architecture: string;
timestamp: number;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Critical: timestamp type mismatches backend payload.

Backend sets Timestamp: time.Now() (RFC3339 string). Here timestamp is number. This will break parsing.

Choose one:

  • Preferred: change FE to timestamp: string.
  • Or change BE to send epoch millis (int64) and keep FE as number.

FE fix:

 export interface SystemStatsType {
   os_type: string;
   hostname: string;
   cpu_info: string;
   cpu_cores: number;
   memory: MemoryStats;
   load: LoadStats;
   disk: DiskStats;
   kernel_version: string;
   architecture: string;
-  timestamp: number;
+  timestamp: string;
 }

BE alternative (Go) for consistency if you prefer numbers:

// types.go
type SystemStats struct {
    // ...
-   Timestamp time.Time `json:"timestamp"`
+   Timestamp int64 `json:"timestamp"` // epoch millis
}

// system_stats.go
stats := SystemStats{
    // ...
-   Timestamp: time.Now(),
+   Timestamp: time.Now().UnixMilli(),
}

I'll verify the timestamp type mismatch claim by examining the backend and frontend code.


🏁 Script executed:

#!/bin/bash
# Find backend Go files with SystemStats struct
find . -type f -name "*.go" | head -20

Length of output: 960


🏁 Script executed:

#!/bin/bash
# Search for SystemStats struct definition in Go files
rg -n "type SystemStats struct" --type go

Length of output: 69


🏁 Script executed:

#!/bin/bash
# Search for Timestamp field definitions in Go structs
rg -n "Timestamp.*time\.Time" --type go -A2 -B2

Length of output: 817


🏁 Script executed:

#!/bin/bash
# Search for any custom JSON marshaling related to timestamps
rg -n "MarshalJSON|UnmarshalJSON" --type go -A5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find where SystemStats is being returned/serialized in handlers
rg -n "SystemStats\{" --type go -A10

Length of output: 867


🏁 Script executed:

#!/bin/bash
# Check the FE monitor.ts file for how timestamp is used
rg -n "timestamp" view/redux/types/monitor.ts -B2 -A2

Length of output: 84


🏁 Script executed:

#!/bin/bash
# Search for timestamp parsing/conversion in FE code
rg -n "\.timestamp" --type ts --type tsx -C3

Length of output: 27


🏁 Script executed:

#!/bin/bash
# Find the endpoint handler that returns SystemStats
rg -n "func.*SystemStats" --type go -A15

Length of output: 1199


🏁 Script executed:

#!/bin/bash
# Search for dashboard controller/handler files
find . -type f -name "*dashboard*" -name "*.go" | grep -E "(controller|handler|route)"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for any middleware or wrapper that transforms the response
rg -n "json\.Marshal|json\.Unmarshal|ResponseWriter" api/internal/features/dashboard/ --type go -B2 -A2

Length of output: 754


Critical: timestamp type mismatch breaks payload parsing at runtime.

Backend Go struct defines Timestamp time.Time (api/internal/features/dashboard/types.go:55) serialized via standard json.Marshal(), which converts time.Time to RFC3339 strings (e.g., "2025-10-25T14:30:00Z"). Frontend expects timestamp: number, causing parse failure.

Choose one fix:

  • Preferred: Change FE to timestamp: string
  • Alternative: Change BE to epoch millis (int64)

FE fix:

 export interface SystemStatsType {
   os_type: string;
   hostname: string;
   cpu_info: string;
   cpu_cores: number;
   memory: MemoryStats;
   load: LoadStats;
   disk: DiskStats;
   kernel_version: string;
   architecture: string;
-  timestamp: number;
+  timestamp: string;
 }

BE alternative (api/internal/features/dashboard/types.go):

type SystemStats struct {
    // ...
-   Timestamp time.Time `json:"timestamp"`
+   Timestamp int64 `json:"timestamp"` // epoch millis
}

Then update api/internal/features/dashboard/system_stats.go:44:

-   Timestamp: time.Now(),
+   Timestamp: time.Now().UnixMilli(),
🤖 Prompt for AI Agents
In view/redux/types/monitor.ts around lines 59 to 70, the frontend
SystemStatsType declares timestamp as a number but the backend sends an RFC3339
string; update the FE type to timestamp: string to match the serialized
time.Time from the backend, and search/update any places that consume or parse
SystemStatsType.timestamp (parsing to Date or number where needed) so they
handle a string RFC3339 timestamp correctly.

@zhravan
Copy link
Collaborator Author

zhravan commented Oct 25, 2025

@coderabbitai: Please update the title, summary, and description

@coderabbitai coderabbitai bot changed the title feat: add Server/HTTP metrics to dashboard feat: enhance dashboard with draggable layout, charts, and extended system metrics Oct 25, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 25, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@zhravan: I've updated the PR title and description to accurately reflect the changes in this pull request. The new title is:

"feat: enhance dashboard with draggable layout, charts, and extended system metrics"

The description now includes:

  • ✨ Overview of dashboard UX improvements with drag-and-drop functionality
  • 📊 Details about the new chart visualizations (bar chart and doughnut chart)
  • 🖥️ List of extended system metrics (hostname, CPU cores, kernel version, architecture)
  • 🔧 Backend changes to the SystemStats struct
  • 🌍 I18n updates across multiple locales
  • 🧩 New reusable components added
  • 📦 New dependencies

The description provides a comprehensive view of what this PR accomplishes, making it much easier for reviewers to understand the scope and impact of the changes.


🏁 Script executed:

#!/bin/bash

# Update PR title and description
gh pr edit 536 --repo raghavyuva/nixopus \
  --title "feat: enhance dashboard with draggable layout, charts, and extended system metrics" \
  --body "## 📋 Description

This PR significantly enhances the dashboard with an improved user experience through drag-and-drop card reordering, interactive charts, and expanded system information.

## ✨ Key Changes

### 🎨 Dashboard UX Improvements
- **Draggable Grid Layout**: Implemented drag-and-drop functionality for dashboard cards using @dnd-kit
  - Cards can be reordered via drag-and-drop
  - Layout order persists in localStorage
  - Added \"Reset Layout\" button to restore default order
  - Added drag hint banner for user guidance

### 📊 Enhanced Visualizations
- **Load Average Card**: Replaced text display with interactive bar chart showing 1min, 5min, and 15min load averages
- **Memory Usage Card**: Added doughnut chart visualization with used/free memory breakdown
- **System Info Card**: Redesigned with grid layout and comprehensive system details

### 🖥️ Extended System Metrics
Added new system information fields:
- Hostname
- CPU Cores count
- Kernel Version
- Architecture
- Enhanced memory display with total/used/free breakdown

### 🔧 Backend Changes
- Extended \`SystemStats\` struct in Go backend to include hostname, CPU cores, kernel version, and architecture
- Improved system stats collection logic

### 🌍 Internationalization
- Updated locale files for English, Spanish, French, Kannada, and Malayalam
- Added translations for new system metadata labels

### 🧩 New Components
- \`DraggableGrid\`: Reusable drag-and-drop grid component with persistence
- \`BarChartComponent\`: Configurable bar chart component using recharts
- \`DoughnutChartComponent\`: Donut chart component for circular visualizations

## 📦 Dependencies
- Added \`@dnd-kit/core\`, \`@dnd-kit/sortable\`, \`@dnd-kit/utilities\` for drag-and-drop functionality

## 🎯 Scope of Change
- [x] View
- [x] API
- [ ] CLI
- [ ] Infra/Deployment
- [ ] Docs
- [ ] Other

## 🧪 Testing Notes
- Test drag-and-drop functionality across different dashboard cards
- Verify layout persistence after page refresh
- Verify reset layout functionality
- Check chart rendering with different data values
- Test responsiveness across different screen sizes
- Verify all locale translations display correctly"

echo "PR updated successfully!"

Length of output: 71


@zhravan: ✅ Successfully updated PR #536!

The PR now has:

  • New Title: "feat: enhance dashboard with draggable layout, charts, and extended system metrics"
  • Comprehensive Description: Including all key changes, new components, dependencies, scope of changes, and testing notes

You can view the updated PR at: #536

@zhravan zhravan requested a review from raghavyuva October 25, 2025 21:29
<TypographyMuted>{t('dashboard.load.labels.fifteenMin')}</TypographyMuted>
<TypographySmall>{load.fifteenMin.toFixed(2)}</TypographySmall>

{/* Summary Stats with Color Indicators */}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be refactored and reused as a component instead of repeating

<TypographyMuted>{t('dashboard.load.labels.fifteenMin')}</TypographyMuted>
<Skeleton className="h-4 w-12" />
<div className="grid grid-cols-3 gap-2 text-center">
<div>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, can be refactored and reused

});
const { isFeatureEnabled, isLoading: isFeatureFlagsLoading } = useFeatureFlags();
const [showDragHint, setShowDragHint] = React.useState(false);
const [mounted, setMounted] = React.useState(false);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move out of the page to hook for all these logics,

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (5)
view/redux/types/monitor.ts (1)

69-81: Timestamp type mismatch with backend payload (blocks runtime).

FE declares timestamp: number but BE sends RFC3339 string (time.Time). Align both sides: either change FE to string or make BE send Unix millis (int64). Repeating prior finding to keep this visible.

[suggested FE fix]

 export interface SystemStatsType {
   os_type: string;
   hostname: string;
   cpu_info: string;
   cpu_cores: number;
   cpu: CPUStats;
   memory: MemoryStats;
   load: LoadStats;
   disk: DiskStats;
   kernel_version: string;
   architecture: string;
-  timestamp: number;
+  timestamp: string;
 }

—or keep number and update BE (see comment in api/internal/features/dashboard/types.go).

Run to locate uses expecting number:

#!/bin/bash
rg -n --type ts --type tsx -C2 '\btimestamp\b' view | sed -n '1,200p'
api/internal/features/dashboard/types.go (1)

45-57: Standardize timestamp to Unix millis to match FE.

Current: time.Time → JSON string. FE expects number. Prefer sending int64 millis.

[suggested BE change]

 type SystemStats struct {
   OSType        string      `json:"os_type"`
   Hostname      string      `json:"hostname"`
   CPUInfo       string      `json:"cpu_info"`
   CPUCores      int         `json:"cpu_cores"`
   CPU           CPUStats    `json:"cpu"`
   Memory        MemoryStats `json:"memory"`
   Load          LoadStats   `json:"load"`
   Disk          DiskStats   `json:"disk"`
   KernelVersion string      `json:"kernel_version"`
   Architecture  string      `json:"architecture"`
-  Timestamp     time.Time   `json:"timestamp"`
+  Timestamp     int64       `json:"timestamp"` // Unix millis
 }

And set with time.Now().UnixMilli() where populated.

Find constructions to update:

#!/bin/bash
rg -n 'SystemStats\{|\bTimestamp:\s*' api/internal --type go -C3
view/app/dashboard/page.tsx (3)

80-88: Externalize “Reset Layout” and add aria-label.

Use i18n and improve a11y.

-          <Button
+          <Button
             variant="outline"
             size="sm"
             onClick={handleResetLayout}
             className="shrink-0"
+            aria-label={t('dashboard.actions.resetLayout')}
           >
             <RefreshCw className="mr-2 h-4 w-4" />
-            Reset Layout
+            {t('dashboard.actions.resetLayout')}
           </Button>

90-111: Externalize drag-hint banner strings.

Replace hardcoded title/body and button text.

-                <p className="text-sm font-medium text-foreground">Customize Your Dashboard</p>
+                <p className="text-sm font-medium text-foreground">
+                  {t('dashboard.dragHint.title')}
+                </p>
                 <p className="text-xs text-muted-foreground mt-1">
-                  Hover over any card to see the drag handle on the left. Click and drag to rearrange cards in your preferred order. Your layout will be saved automatically.
+                  {t('dashboard.dragHint.body')}
                 </p>
@@
-              Got it
+              {t('common.gotIt')}

127-137: Replace any props with concrete types.

Define a props interface and reuse existing domain types.

-import { useRouter } from 'next/navigation';
+import { useRouter } from 'next/navigation';
+import { SystemStatsType, ContainerData } from '@/redux/types/monitor';
@@
-const MonitoringSection = ({
+interface MonitoringSectionProps {
+  systemStats: SystemStatsType | null;
+  containersData: ContainerData[];
+  t: (key: string, opts?: any) => string;
+  layoutResetKey: number;
+}
+
+const MonitoringSection = ({
   systemStats,
   containersData,
   t,
   layoutResetKey
-}: {
-  systemStats: any;
-  containersData: any;
-  t: any;
-  layoutResetKey: number;
-}) => {
+}: MonitoringSectionProps) => {

Run to confirm no lingering any in this file and to find ContainerData if path differs:

#!/bin/bash
rg -nP --type=ts -C2 '\bMonitoringSection\s*\(' view/app/dashboard/page.tsx
fd -t f -e ts -e tsx 'monitor' | xargs rg -nP 'interface\s+ContainerData|type\s+ContainerData'
🧹 Nitpick comments (19)
view/app/dashboard/components/system/system-metric-card.tsx (1)

25-36: Minor a11y: hide decorative icon and signal loading.

  • Mark the icon decorative to avoid SR noise.
  • Expose loading via aria-busy.
-        <CardTitle className="text-xs sm:text-sm font-medium flex items-center">
-          <Icon className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2 text-muted-foreground" />
+        <CardTitle className="text-xs sm:text-sm font-medium flex items-center">
+          <Icon aria-hidden="true" className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2 text-muted-foreground" />
           <TypographySmall>{title}</TypographySmall>
         </CardTitle>
       </CardHeader>
-      <CardContent className="flex-1">
+      <CardContent className="flex-1" aria-busy={isLoading || undefined}>
         {content}
       </CardContent>
view/lib/i18n/locales/kn.json (1)

788-794: CPU and system labels are correct; fix 15 pre-existing typos in notifications section.

The additions at lines 788-794 and 812-826 are properly formatted with no issues. The file contains 15 pre-existing instances of "ಅ th" (corrupted text) in the notifications section (lines 465-563) that should be corrected for translation quality:

Lines 465, 466, 469, 476, 477, 486, 487, 493, 513, 517, 526, 542, 559, 560, 563.

view/app/dashboard/hooks/use-system-metric.ts (1)

11-15: Widen t typing to support i18n params

Your useTranslation() exposes t(key, params?). The narrowed (key: string) => string blocks param usage at call sites.

-interface UseSystemMetricResult<T> {
-  data: T;
-  isLoading: boolean;
-  t: (key: string) => string;
-}
+interface UseSystemMetricResult<T> {
+  data: T;
+  isLoading: boolean;
+  t: (key: string, params?: Record<string, string>) => string;
+}
view/app/dashboard/components/system/cpu-usage.tsx (3)

45-47: Clamp overall usage to [0, 100]

Avoid odd values from backend rounding.

-            {formatPercentage(cpu?.overall || 0)}%
+            {formatPercentage(Math.min(100, Math.max(0, cpu?.overall ?? 0)))}%

65-76: Hoist color palette out of the map

Minor perf/readability nit: avoid re-allocating colors for each core.

-            {topCores.length > 0 && (
+            {topCores.length > 0 && (
               <div className="grid grid-cols-3 gap-2 text-center">
-                {topCores.map((core) => {
-                  const colors = [
+                {(() => {
+                  const colors = [
                     CHART_COLORS.blue,
                     CHART_COLORS.green,
                     CHART_COLORS.orange,
                     CHART_COLORS.purple,
                     CHART_COLORS.red,
                     CHART_COLORS.yellow,
-                  ];
-                  const color = colors[core.core_id % colors.length];
-
-                  return (
+                  ];
+                  return topCores.map((core) => {
+                    const color = colors[core.core_id % colors.length];
+                    return (
                       <div key={core.core_id} className="flex flex-col items-center gap-1">
                         <div className="flex items-center gap-1">
                           <div className="h-2 w-2 rounded-full" style={{ backgroundColor: color }} />
                           <TypographyMuted className="text-xs">Core {core.core_id}</TypographyMuted>
                         </div>
                         <TypographySmall className="text-sm font-bold">
                           {formatPercentage(core.usage)}%
                         </TypographySmall>
                       </div>
-                  );
-                })}
+                    );
+                  });
+                })()}
               </div>
             )}

111-116: Localize “Overall” in skeleton

Keep skeletons translatable.

-function CPUUsageCardSkeletonContent() {
-  return (
+function CPUUsageCardSkeletonContent() {
+  const { t } = useSystemMetric({
+    systemStats: null,
+    extractData: (s) => s.cpu,
+    defaultData: DEFAULT_METRICS.cpu,
+  });
+  return (
     <div className="space-y-4">
       <div className="text-center">
-        <TypographyMuted className="text-xs">Overall</TypographyMuted>
+        <TypographyMuted className="text-xs">{t('dashboard.cpu.overall')}</TypographyMuted>
view/app/dashboard/components/system/constants.ts (1)

27-33: Avoid any[] for disk.allMounts

Type this to match DiskUsage expectations.

-    allMounts: [] as any[]
+    allMounts: [] as Array<{ mountPoint: string; size: string; used: string; capacity: string }>
view/app/dashboard/components/system/disk-usage.tsx (2)

41-46: Clamp progress width

Guard against out-of-range percentages.

-          <div
-            className={`h-2 rounded-full bg-primary`}
-            style={{ width: `${disk.percentage}%` }}
-          />
+          <div
+            className="h-2 rounded-full bg-primary"
+            style={{ width: `${Math.min(100, Math.max(0, disk.percentage))}%` }}
+            aria-valuenow={Math.min(100, Math.max(0, disk.percentage))}
+            aria-valuemin={0}
+            aria-valuemax={100}
+            role="progressbar"
+          />

48-56: Use i18n params instead of manual string replace

The t() function supports parameterized replacement via optional second argument: t(key, { value }) instead of t(key).replace('{value}', ...). This is cleaner and matches the i18n hook implementation.

          <TypographyMuted className="text-xs truncate max-w-[80px] sm:max-w-[100px]">
-            {t('dashboard.disk.used').replace('{value}', disk.used.toFixed(2))}
+            {t('dashboard.disk.used', { value: disk.used.toFixed(2) })}
          </TypographyMuted>
          <TypographyMuted className="text-xs truncate max-w-[60px] sm:max-w-[80px]">
-            {t('dashboard.disk.percentage').replace('{value}', formatPercentage(disk.percentage))}
+            {t('dashboard.disk.percentage', { value: formatPercentage(disk.percentage) })}
          </TypographyMuted>
          <TypographyMuted className="text-xs truncate max-w-[80px] sm:max-w-[100px]">
-            {t('dashboard.disk.total').replace('{value}', disk.total.toFixed(2))}
+            {t('dashboard.disk.total', { value: disk.total.toFixed(2) })}
          </TypographyMuted>

Note: Similar patterns exist in other files (EditUserDialog.tsx, log-viewer.tsx, header.tsx, deploymentStatusChart.tsx) and should be normalized across the codebase.

view/app/dashboard/components/system/system-stats.tsx (1)

27-46: Memoize systemStatsItems to stabilize references and prevent unnecessary reorder effect runs

systemStatsItems is recreated each render. DraggableGrid's useEffect depends on the items prop (line 122 in draggable-grid.tsx), so a new reference triggers effect reruns and repeated localStorage reorder application. Memoize with useMemo and systemStats dependency to prevent this.

Optionally add gridCols prop for responsive layout (consistent with dashboard usage).

view/lib/i18n/locales/en.json (1)

808-814: CPU labels added — looks good. Add complementary keys used by charts/tooltips.

To fully support the new chart UIs and remove hardcoded strings, please add:

  • dashboard.memory.free
  • dashboard.load.axis.x, dashboard.load.axis.y
  • dashboard.actions.resetLayout
  • dashboard.dragHint.title, dashboard.dragHint.body
  • common.gotIt

Example patch:

   "dashboard": {
+    "actions": {
+      "resetLayout": "Reset Layout"
+    },
     "load": {
       "title": "Load Average",
+      "axis": { "x": "Time Period", "y": "Load" },
       "labels": {
         "oneMin": "1 minute",
         "fiveMin": "5 minutes",
         "fifteenMin": "15 minutes"
       }
     },
     "cpu": {
       "title": "CPU Usage (per core)",
       "overall": "Overall",
       "usage": "Usage (%)",
       "cores": "Cores",
       "perCore": "Per Core"
     },
     "memory": {
       "title": "Memory Usage",
       "used": "{value} GB used",
+      "free": "{value} GB free",
       "total": "{value} GB total",
       "percentage": "{value}%"
     },
+    "dragHint": {
+      "title": "Customize Your Dashboard",
+      "body": "Hover over any card to see the drag handle on the left. Click and drag to rearrange cards. Your layout will be saved automatically."
+    },

And at root:

   "common": {
     "readLess": "Read less",
+    "gotIt": "Got it"
   },
view/app/dashboard/components/system/memory-usage.tsx (1)

41-47: Externalize memory labels and units to i18n.

Replace hardcoded "Used/Free/Total" and center label with locale strings (keys added in en.json comment).

-            centerLabel={{
-              value: `${memory.percentage.toFixed(1)}%`,
-              subLabel: 'Used'
-            }}
+            centerLabel={{
+              value: `${memory.percentage.toFixed(1)}%`,
+              subLabel: t('dashboard.memory.used').replace('{value}', '')
+            }}
@@
-              <TypographyMuted>
-                Used: {formatGB(memory.used)} GB
-              </TypographyMuted>
+              <TypographyMuted>
+                {t('dashboard.memory.used', { value: formatGB(memory.used) })}
+              </TypographyMuted>
@@
-              <TypographyMuted>
-                Free: {formatGB(freeMemory)} GB
-              </TypographyMuted>
+              <TypographyMuted>
+                {t('dashboard.memory.free', { value: formatGB(freeMemory) })}
+              </TypographyMuted>
@@
-          <TypographyMuted className="text-xs text-center">
-            Total: {formatGB(memory.total)} GB
-          </TypographyMuted>
+          <TypographyMuted className="text-xs text-center">
+            {t('dashboard.memory.total', { value: formatGB(memory.total) })}
+          </TypographyMuted>

Also applies to: 56-68, 71-74

view/app/dashboard/components/system/load-average.tsx (3)

35-35: Remove spacer
s; use CSS utility spacing instead.

-      <br /><br /><br />

41-45: Externalize axis and period labels.

Use i18n keys so tooltips/text are localized.

-            yAxisLabel="Load"
-            xAxisLabel="Time Period"
+            yAxisLabel={t('dashboard.load.axis.y')}
+            xAxisLabel={t('dashboard.load.axis.x')}
@@
-              <TypographyMuted className="text-xs">1 min</TypographyMuted>
+              <TypographyMuted className="text-xs">
+                {t('dashboard.load.labels.oneMin')}
+              </TypographyMuted>
@@
-              <TypographyMuted className="text-xs">5 min</TypographyMuted>
+              <TypographyMuted className="text-xs">
+                {t('dashboard.load.labels.fiveMin')}
+              </TypographyMuted>
@@
-              <TypographyMuted className="text-xs">15 min</TypographyMuted>
+              <TypographyMuted className="text-xs">
+                {t('dashboard.load.labels.fifteenMin')}
+              </TypographyMuted>

Also applies to: 49-70


25-27: Optional: feed localized names into chart data for tooltip parity.

Consider passing labels into createLoadAverageChartData so bar names match localized text. See utils.ts comment for a backward‑compatible signature change.

view/app/dashboard/components/system/utils.ts (4)

10-30: Allow localized labels in load-average helpers (keep English defaults).

Make functions accept optional labels to support i18n tooltips without breaking callers.

-export const createLoadAverageChartData = (load: {
+export const createLoadAverageChartData = (load: {
   oneMin: number;
   fiveMin: number;
   fifteenMin: number;
-}): BarChartDataItem[] => [
+}, labels?: { oneMin?: string; fiveMin?: string; fifteenMin?: string }): BarChartDataItem[] => [
   {
-    name: '1 min',
+    name: labels?.oneMin ?? '1 min',
     value: load.oneMin,
     fill: CHART_COLORS.blue,
   },
   {
-    name: '5 min',
+    name: labels?.fiveMin ?? '5 min',
     value: load.fiveMin,
     fill: CHART_COLORS.green,
   },
   {
-    name: '15 min',
+    name: labels?.fifteenMin ?? '15 min',
     value: load.fifteenMin,
     fill: CHART_COLORS.orange,
   },
 ];
 
-export const createLoadAverageChartConfig = () => ({
+export const createLoadAverageChartConfig = (labels?: { oneMin?: string; fiveMin?: string; fifteenMin?: string }) => ({
   oneMin: {
-    label: '1 min',
+    label: labels?.oneMin ?? '1 min',
     color: CHART_COLORS.blue,
   },
   fiveMin: {
-    label: '5 min',
+    label: labels?.fiveMin ?? '5 min',
     color: CHART_COLORS.green,
   },
   fifteenMin: {
-    label: '15 min',
+    label: labels?.fifteenMin ?? '15 min',
     color: CHART_COLORS.orange,
   },
 });

Also applies to: 32-45


47-72: Same for memory helpers: inject labels to localize chart/tooltip.

-export const createMemoryChartData = (
-  used: number,
-  free: number
-): DoughnutChartDataItem[] => [
+export const createMemoryChartData = (
+  used: number,
+  free: number,
+  labels?: { used?: string; free?: string }
+): DoughnutChartDataItem[] => [
   {
-    name: 'Used',
+    name: labels?.used ?? 'Used',
     value: used,
     fill: CHART_COLORS.blue,
   },
   {
-    name: 'Free',
+    name: labels?.free ?? 'Free',
     value: free,
     fill: CHART_COLORS.green,
   },
 ];
 
-export const createMemoryChartConfig = () => ({
+export const createMemoryChartConfig = (labels?: { used?: string; free?: string }) => ({
   used: {
-    label: 'Used Memory',
+    label: labels?.used ?? 'Used Memory',
     color: CHART_COLORS.blue,
   },
   free: {
-    label: 'Free Memory',
+    label: labels?.free ?? 'Free Memory',
     color: CHART_COLORS.green,
   },
 });

75-97: CPU helpers: expose a label formatter for per‑core names.

Support localization or custom naming while keeping defaults.

-export const createCPUChartData = (
-  perCore: Array<{ core_id: number; usage: number }>
-): BarChartDataItem[] => {
+export const createCPUChartData = (
+  perCore: Array<{ core_id: number; usage: number }>,
+  labelForCore?: (coreId: number) => string
+): BarChartDataItem[] => {
@@
-  return perCore.map((core) => ({
-    name: `Core ${core.core_id}`,
+  return perCore.map((core) => ({
+    name: labelForCore ? labelForCore(core.core_id) : `Core ${core.core_id}`,
     value: core.usage,
     fill: colors[core.core_id % colors.length],
   }));
};
 
-export const createCPUChartConfig = (coreCount: number) => {
+export const createCPUChartConfig = (coreCount: number, labelForCore?: (coreIndex: number) => string) => {
@@
   for (let i = 0; i < coreCount; i++) {
     config[`core${i}`] = {
-      label: `Core ${i}`,
+      label: labelForCore ? labelForCore(i) : `Core ${i}`,
       color: colors[i % colors.length],
     };
   }

Also applies to: 99-118


6-7: Minor: guard formatters against NaN.

-export const formatGB = (value: number): string => `${value.toFixed(2)}`;
-export const formatPercentage = (value: number): string => `${value.toFixed(1)}`;
+export const formatGB = (value: number): string =>
+  Number.isFinite(value) ? value.toFixed(2) : '0.00';
+export const formatPercentage = (value: number): string =>
+  Number.isFinite(value) ? value.toFixed(1) : '0.0';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39d9b03 and 86b7e35.

⛔ Files ignored due to path filters (1)
  • view/yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (19)
  • api/api/versions.json (1 hunks)
  • api/internal/features/dashboard/system_stats.go (2 hunks)
  • api/internal/features/dashboard/types.go (1 hunks)
  • view/app/dashboard/components/system/constants.ts (1 hunks)
  • view/app/dashboard/components/system/cpu-usage.tsx (1 hunks)
  • view/app/dashboard/components/system/disk-usage.tsx (3 hunks)
  • view/app/dashboard/components/system/load-average.tsx (1 hunks)
  • view/app/dashboard/components/system/memory-usage.tsx (1 hunks)
  • view/app/dashboard/components/system/system-metric-card.tsx (1 hunks)
  • view/app/dashboard/components/system/system-stats.tsx (2 hunks)
  • view/app/dashboard/components/system/utils.ts (1 hunks)
  • view/app/dashboard/hooks/use-system-metric.ts (1 hunks)
  • view/app/dashboard/page.tsx (5 hunks)
  • view/lib/i18n/locales/en.json (2 hunks)
  • view/lib/i18n/locales/es.json (2 hunks)
  • view/lib/i18n/locales/fr.json (2 hunks)
  • view/lib/i18n/locales/kn.json (2 hunks)
  • view/lib/i18n/locales/ml.json (2 hunks)
  • view/redux/types/monitor.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • view/lib/i18n/locales/es.json
  • api/internal/features/dashboard/system_stats.go
  • api/api/versions.json
🧰 Additional context used
🧬 Code graph analysis (11)
view/app/dashboard/hooks/use-system-metric.ts (2)
view/redux/types/monitor.ts (1)
  • SystemStatsType (69-81)
view/hooks/use-translation.ts (1)
  • useTranslation (18-74)
view/app/dashboard/components/system/cpu-usage.tsx (7)
view/redux/types/monitor.ts (1)
  • SystemStatsType (69-81)
view/app/dashboard/hooks/use-system-metric.ts (1)
  • useSystemMetric (21-41)
view/app/dashboard/components/system/constants.ts (2)
  • DEFAULT_METRICS (12-33)
  • CHART_COLORS (2-9)
view/app/dashboard/components/system/utils.ts (3)
  • createCPUChartData (75-97)
  • createCPUChartConfig (99-118)
  • formatPercentage (7-7)
view/app/dashboard/components/system/system-metric-card.tsx (1)
  • SystemMetricCard (16-38)
view/components/ui/typography.tsx (2)
  • TypographyMuted (114-122)
  • TypographySmall (99-112)
view/components/ui/bar-chart-component.tsx (1)
  • BarChartComponent (22-80)
view/app/dashboard/components/system/system-stats.tsx (2)
view/app/dashboard/components/system/cpu-usage.tsx (1)
  • CPUUsageCardSkeleton (129-146)
view/components/ui/draggable-grid.tsx (2)
  • DraggableItem (26-30)
  • DraggableGrid (93-235)
view/app/dashboard/components/system/system-metric-card.tsx (1)
view/components/ui/typography.tsx (1)
  • TypographySmall (99-112)
view/app/dashboard/components/system/memory-usage.tsx (8)
view/redux/types/monitor.ts (1)
  • SystemStatsType (69-81)
view/app/dashboard/hooks/use-system-metric.ts (1)
  • useSystemMetric (21-41)
view/app/dashboard/components/system/constants.ts (2)
  • DEFAULT_METRICS (12-33)
  • CHART_COLORS (2-9)
view/app/dashboard/components/system/utils.ts (3)
  • createMemoryChartData (47-61)
  • createMemoryChartConfig (63-72)
  • formatGB (6-6)
view/app/dashboard/components/system/system-metric-card.tsx (1)
  • SystemMetricCard (16-38)
view/components/ui/doughnut-chart-component.tsx (1)
  • DoughnutChartComponent (25-82)
view/components/ui/typography.tsx (1)
  • TypographyMuted (114-122)
view/components/ui/skeleton.tsx (1)
  • Skeleton (13-13)
view/app/dashboard/components/system/utils.ts (3)
view/components/ui/bar-chart-component.tsx (1)
  • BarChartDataItem (7-11)
view/app/dashboard/components/system/constants.ts (1)
  • CHART_COLORS (2-9)
view/components/ui/doughnut-chart-component.tsx (1)
  • DoughnutChartDataItem (7-11)
view/redux/types/monitor.ts (1)
api/internal/features/dashboard/types.go (5)
  • CPUCore (59-62)
  • CPUStats (64-67)
  • MemoryStats (69-74)
  • LoadStats (76-81)
  • DiskStats (92-99)
view/app/dashboard/page.tsx (2)
view/app/dashboard/components/smtp-banner.tsx (1)
  • SMTPBanner (13-51)
view/components/ui/draggable-grid.tsx (2)
  • DraggableItem (26-30)
  • DraggableGrid (93-235)
view/app/dashboard/components/system/load-average.tsx (7)
view/redux/types/monitor.ts (1)
  • SystemStatsType (69-81)
view/app/dashboard/hooks/use-system-metric.ts (1)
  • useSystemMetric (21-41)
view/app/dashboard/components/system/constants.ts (2)
  • DEFAULT_METRICS (12-33)
  • CHART_COLORS (2-9)
view/app/dashboard/components/system/utils.ts (2)
  • createLoadAverageChartData (10-30)
  • createLoadAverageChartConfig (32-45)
view/app/dashboard/components/system/system-metric-card.tsx (1)
  • SystemMetricCard (16-38)
view/components/ui/bar-chart-component.tsx (1)
  • BarChartComponent (22-80)
view/components/ui/typography.tsx (2)
  • TypographyMuted (114-122)
  • TypographySmall (99-112)
view/app/dashboard/components/system/disk-usage.tsx (6)
view/app/dashboard/hooks/use-system-metric.ts (1)
  • useSystemMetric (21-41)
view/app/dashboard/components/system/constants.ts (1)
  • DEFAULT_METRICS (12-33)
view/app/dashboard/components/system/system-metric-card.tsx (1)
  • SystemMetricCard (16-38)
view/components/ui/typography.tsx (2)
  • TypographyMuted (114-122)
  • TypographySmall (99-112)
view/app/dashboard/components/system/utils.ts (1)
  • formatPercentage (7-7)
view/hooks/use-translation.ts (1)
  • useTranslation (18-74)
api/internal/features/dashboard/types.go (1)
view/redux/types/monitor.ts (5)
  • CPUStats (64-67)
  • MemoryStats (27-32)
  • LoadStats (34-39)
  • DiskStats (50-57)
  • CPUCore (59-62)
🔇 Additional comments (4)
view/lib/i18n/locales/fr.json (1)

792-798: Keys align; verify parity with base locale.

CPU and system label additions look correct and keep placeholder keys intact. Please verify this set matches en.json to avoid missing labels at runtime.

Also applies to: 816-830

view/lib/i18n/locales/ml.json (1)

807-813: Incorrect line numbers in review. The specified sections do not contain the problematic placeholders.

Lines 807-813 (CPU section) and 831-845 (system labels) contain only localized text with no interpolation placeholders—neither have the non-ASCII placeholder issue.

However, the broader concern is valid: non-ASCII placeholders like {എണ്ണുക}, {മൂല്യം}, {പേര്}, {നമ്പർ}, {നിലവിലുള്ളത്}, and {നിലവിലെ}/{ആകെ} exist throughout the file (lines 68, 689–690, 781, 787–789, 816–818, 862, 914–915, 948, 1109–1110, 1140, 1319) and must be normalized to canonical English keys for i18next interpolation to work.

Likely an incorrect or invalid review comment.

view/app/dashboard/components/system/system-stats.tsx (1)

17-23: Skeleton path extension looks good

CPU skeleton aligns with other metric placeholders and keeps layout stable while loading.

view/lib/i18n/locales/en.json (1)

832-846: System label map is consistent. Ensure all locales mirror this structure.

Please mirror dashboard.system.labels across es/fr/kn/ml to avoid runtime misses.

Comment on lines 25 to 28
const perCoreData = cpu?.per_core || [];
const chartData = createCPUChartData(perCoreData);
const chartConfig = createCPUChartConfig(perCoreData.length);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Align chartConfig with the Bar’s dataKey

Bar uses dataKey="value", but createCPUChartConfig returns keys like core0, core1. Tooltips/legend may not resolve.

-  const chartConfig = createCPUChartConfig(perCoreData.length);
+  const chartConfig = {
+    value: { label: t('dashboard.cpu.usage'), color: CHART_COLORS.blue }
+  };

Confirm what ChartContainer/ChartTooltipContent expect:


🏁 Script executed:

#!/bin/bash
rg -n -C3 "export (const|function) ChartContainer|ChartTooltipContent" --type=ts --type=tsx

Length of output: 27



---

🏁 Script executed:

```shell
#!/bin/bash
# Search for ChartContainer and ChartTooltipContent exports without file type restriction
rg -n -C3 "export (const|function) (ChartContainer|ChartTooltipContent)" -g "*.ts" -g "*.tsx"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for Bar component usage with dataKey in the cpu-usage file
rg -n "dataKey" view/app/dashboard/components/system/cpu-usage.tsx -A2 -B2

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find createCPUChartConfig function definition
rg -n "createCPUChartConfig" --type=ts -A10

Length of output: 2777



---

🏁 Script executed:

```shell
#!/bin/bash
# Get complete createCPUChartConfig function definition
rg -n "export const createCPUChartConfig" -A25 view/app/dashboard/components/system/utils.ts

Length of output: 544


🏁 Script executed:

#!/bin/bash
# Get the complete cpu-usage.tsx file to see Bar component and how chartConfig is used
cat view/app/dashboard/components/system/cpu-usage.tsx

Length of output: 4877


🏁 Script executed:

#!/bin/bash
# Check createCPUChartData function as well
rg -n "export const createCPUChartData" -A20 view/app/dashboard/components/system/utils.ts

Length of output: 609



---

🏁 Script executed:

```shell
#!/bin/bash
# Find BarChartComponent definition
fd -t f "bar-chart-component" -e ts -e tsx

Length of output: 42


🏁 Script executed:

#!/bin/bash
# Get BarChartComponent implementation
rg -n "export.*BarChartComponent" -A30 -g "*.ts" -g "*.tsx"

Length of output: 2462



---

🏁 Script executed:

```shell
#!/bin/bash
# Get full BarChartComponent file
cat view/components/ui/bar-chart-component.tsx

Length of output: 2417


🏁 Script executed:

#!/bin/bash
# Check the skeleton hardcoded "Overall" string - not translated
rg -n "Overall" view/app/dashboard/components/system/cpu-usage.tsx

Length of output: 111


Fix chartConfig keys and add missing i18n translations

The chartConfig keys (core0, core1, etc.) don't align with the Bar's dataKey="value". ChartTooltipContent expects config keys matching the data field names. Since the Bar reads from dataKey="value", the config should be keyed by "value":

-  const chartConfig = createCPUChartConfig(perCoreData.length);
+  const chartConfig = {
+    value: { label: t('dashboard.cpu.usage'), color: CHART_COLORS.blue }
+  };

Also, the skeleton has a hardcoded untranslated string at line 111:

-        <TypographyMuted className="text-xs">Overall</TypographyMuted>
+        <TypographyMuted className="text-xs">{t('dashboard.cpu.overall')}</TypographyMuted>

As a secondary improvement, the colors array is duplicated in three places (utils.ts lines 101–108 and 83–90, cpu-usage.tsx lines 67–73). Consider moving it to constants for maintainability.

📝 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.

Suggested change
const perCoreData = cpu?.per_core || [];
const chartData = createCPUChartData(perCoreData);
const chartConfig = createCPUChartConfig(perCoreData.length);
const perCoreData = cpu?.per_core || [];
const chartData = createCPUChartData(perCoreData);
const chartConfig = {
value: { label: t('dashboard.cpu.usage'), color: CHART_COLORS.blue }
};
🤖 Prompt for AI Agents
In view/app/dashboard/components/system/cpu-usage.tsx around lines 25 to 28,
change the chartConfig so its keys align with the Bar's dataKey ("value")
instead of per-core keys (core0, core1, ...): produce a config keyed by "value"
(and preserve any per-core color mapping elsewhere if needed) so
ChartTooltipContent can look up labels/formatters by the same field name the Bar
uses; also fix the untranslated hardcoded string at line 111 by replacing it
with the appropriate i18n key/translation call; finally, deduplicate the colors
array by moving the shared color definitions into a single exported constant
(e.g., a colors constant in utils.ts or a new constants file) and import it from
cpu-usage.tsx and the two utils.ts locations (lines ~83–90 and ~101–108) so all
three sites use the same source of truth.

Comment on lines 25 to 27
// Calculate free memory
const freeMemory = memory.total - memory.used;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clamp free memory to avoid negative slices.

If used > total (transient/reporting skew), free becomes negative and the chart may misrender. Clamp to zero.

-  const freeMemory = memory.total - memory.used;
+  const freeMemory = Math.max(0, memory.total - memory.used);
📝 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.

Suggested change
// Calculate free memory
const freeMemory = memory.total - memory.used;
// Calculate free memory
const freeMemory = Math.max(0, memory.total - memory.used);
🤖 Prompt for AI Agents
In view/app/dashboard/components/system/memory-usage.tsx around lines 25 to 27,
the freeMemory calculation can become negative when memory.used > memory.total
causing chart misrendering; change the calculation to clamp the result to zero
(e.g., set freeMemory to the max of 0 and memory.total - memory.used) so slices
never receive negative values and ensure the variable remains a number for
downstream rendering.

@zhravan
Copy link
Collaborator Author

zhravan commented Oct 26, 2025

image

@zhravan zhravan requested a review from raghavyuva October 26, 2025 21:50
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
view/app/dashboard/page.tsx (3)

60-68: Externalize hardcoded UI text to i18n.

The "Reset Layout" button text should use t(...) for consistency with the rest of the page and to support internationalization.

Apply this diff:

 <Button
   variant="outline"
   size="sm"
   onClick={handleResetLayout}
   className="shrink-0"
+  aria-label={t('dashboard.actions.resetLayout')}
 >
   <RefreshCw className="mr-2 h-4 w-4" />
-  Reset Layout
+  {t('dashboard.actions.resetLayout')}
 </Button>

Ensure the key dashboard.actions.resetLayout exists in locale files.


70-92: Externalize hardcoded drag hint text to i18n.

The drag hint banner contains hardcoded English strings: "Customize Your Dashboard", the hint paragraph, and "Got it". These should use t(...) for internationalization support.

Apply this diff:

 {mounted && showDragHint && (
   <div className="mb-4 p-4 bg-primary/5 border border-primary/20 rounded-lg flex items-start justify-between gap-4">
     <div className="flex items-start gap-3">
       <div className="mt-0.5 text-primary">
         <Info className="h-5 w-5" />
       </div>
       <div className="flex-1">
-        <p className="text-sm font-medium text-foreground">Customize Your Dashboard</p>
+        <p className="text-sm font-medium text-foreground">
+          {t('dashboard.dragHint.title')}
+        </p>
         <p className="text-xs text-muted-foreground mt-1">
-          Hover over any card to see the drag handle on the left. Click and drag to rearrange cards in your preferred order. Your layout will be saved automatically.
+          {t('dashboard.dragHint.body')}
         </p>
       </div>
     </div>
     <Button
       variant="ghost"
       size="default"
       onClick={dismissHint}
       className="shrink-0"
     >
-      Got it
+      {t('common.gotIt')}
     </Button>
   </div>
 )}

Ensure these keys exist in locale files:

  • dashboard.dragHint.title
  • dashboard.dragHint.body
  • common.gotIt

107-117: Replace any types with proper type definitions.

The MonitoringSection component uses any types for its props, which defeats the purpose of TypeScript's type safety and increases the risk of runtime errors.

Apply this diff:

+import { SystemStatsType } from '@/redux/types/monitor';
+import type { ContainerData } from '@/redux/types/monitor';
+import { translationKey } from '@/hooks/use-translation';
+
+interface MonitoringSectionProps {
+  systemStats: SystemStatsType | null;
+  containersData: ContainerData[];
+  t: (key: translationKey, params?: Record<string, string>) => string;
+  layoutResetKey: number;
+}
+
 const MonitoringSection = ({
   systemStats,
   containersData,
   t,
   layoutResetKey
-}: {
-  systemStats: any;
-  containersData: any;
-  t: any;
-  layoutResetKey: number;
-}) => {
+}: MonitoringSectionProps) => {
🧹 Nitpick comments (3)
view/app/dashboard/hooks/use-dashboard.ts (2)

32-40: Consider extracting localStorage keys as constants.

The localStorage keys 'dashboard-drag-hint-seen' and 'dashboard-card-order' are hardcoded strings. Extracting them as named constants would improve maintainability and reduce typo risks.

Apply this diff:

+'use client';
+
+import React from 'react';
+import { useFeatureFlags } from '@/hooks/features_provider';
+import { useAppSelector } from '@/redux/hooks';
+import { useGetSMTPConfigurationsQuery } from '@/redux/services/settings/notificationApi';
+import { FeatureNames } from '@/types/feature-flags';
+import useMonitor from './use-monitor';
+
+const STORAGE_KEYS = {
+  DRAG_HINT_SEEN: 'dashboard-drag-hint-seen',
+  CARD_ORDER: 'dashboard-card-order',
+} as const;
+
 export function useDashboard() {
   const { isFeatureEnabled, isLoading: isFeatureFlagsLoading } = useFeatureFlags();
   const { containersData, systemStats } = useMonitor();
   const activeOrganization = useAppSelector((state) => state.user.activeOrganization);

   const { data: smtpConfig } = useGetSMTPConfigurationsQuery(activeOrganization?.id, {
     skip: !activeOrganization
   });

   const [showDragHint, setShowDragHint] = React.useState(false);
   const [mounted, setMounted] = React.useState(false);
   const [layoutResetKey, setLayoutResetKey] = React.useState(0);

   React.useEffect(() => {
     setMounted(true);
-    const hasSeenHint = localStorage.getItem('dashboard-drag-hint-seen');
+    const hasSeenHint = localStorage.getItem(STORAGE_KEYS.DRAG_HINT_SEEN);
     if (!hasSeenHint) {
       setShowDragHint(true);
     }
   }, []);

   const dismissHint = React.useCallback(() => {
     setShowDragHint(false);
-    localStorage.setItem('dashboard-drag-hint-seen', 'true');
+    localStorage.setItem(STORAGE_KEYS.DRAG_HINT_SEEN, 'true');
   }, []);

   const handleResetLayout = React.useCallback(() => {
-    localStorage.removeItem('dashboard-card-order');
+    localStorage.removeItem(STORAGE_KEYS.CARD_ORDER);
     setLayoutResetKey((prev) => prev + 1);
   }, []);

23-30: The 'use client' directive mitigates SSR risks, but localStorage access could be more explicit.

The 'use client' directive at the top of the file ensures this hook runs only on the client. The rendering in page.tsx is correctly guarded with {mounted && showDragHint &&} to prevent hydration mismatch. However, the pattern of accessing localStorage directly without an explicit client-side check is repeated across dashboard components (smtp-banner.tsx uses the same pattern). While this works with the current setup, best practice would be to add a guard:

React.useEffect(() => {
  setMounted(true);
  if (typeof window !== 'undefined') {
    const hasSeenHint = localStorage.getItem('dashboard-drag-hint-seen');
    if (!hasSeenHint) {
      setShowDragHint(true);
    }
  }
}, []);

This makes the client-side requirement explicit and is more resilient if the code is refactored elsewhere.

view/app/dashboard/hooks/use-system-metric.ts (1)

5-9: Document stability requirements for function and object parameters.

The extractData function and defaultData object/array are included in the useMemo dependencies (line 34), but the hook doesn't enforce or document that these must be stable references. If callers pass inline functions or newly created objects on every render, memoization will be ineffective.

Add JSDoc to the interface to guide consumers:

 interface UseSystemMetricOptions<T> {
+  /** System stats object from the monitor hook */
   systemStats: SystemStatsType | null;
+  /** 
+   * Data extraction function. Must be stable (e.g., useCallback or module-level).
+   * Called only when systemStats changes.
+   */
   extractData: (stats: SystemStatsType) => T;
+  /** 
+   * Default value returned when systemStats is null. Must be stable 
+   * (e.g., useMemo, constant, or module-level) to avoid unnecessary re-renders.
+   */
   defaultData: T;
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 86b7e35 and f04685c.

📒 Files selected for processing (3)
  • view/app/dashboard/hooks/use-dashboard.ts (1 hunks)
  • view/app/dashboard/hooks/use-system-metric.ts (1 hunks)
  • view/app/dashboard/page.tsx (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
view/app/dashboard/hooks/use-system-metric.ts (2)
view/redux/types/monitor.ts (1)
  • SystemStatsType (69-81)
view/hooks/use-translation.ts (2)
  • translationKey (16-16)
  • useTranslation (18-74)
view/app/dashboard/page.tsx (3)
view/app/dashboard/hooks/use-dashboard.ts (1)
  • useDashboard (10-65)
view/app/dashboard/components/smtp-banner.tsx (1)
  • SMTPBanner (13-51)
view/components/ui/draggable-grid.tsx (2)
  • DraggableItem (26-30)
  • DraggableGrid (93-235)
view/app/dashboard/hooks/use-dashboard.ts (2)
view/hooks/features_provider.tsx (1)
  • useFeatureFlags (59-59)
view/redux/hooks.ts (1)
  • useAppSelector (5-5)
🔇 Additional comments (6)
view/app/dashboard/hooks/use-dashboard.ts (1)

46-64: LGTM! Clean return structure with organized exports.

The hook provides a well-organized API surface grouped by concern (feature flags, data, UI state, actions), making it easy for consumers to understand and use.

view/app/dashboard/hooks/use-system-metric.ts (1)

21-41: LGTM! Clean generic hook with proper memoization.

The hook correctly:

  • Derives loading state from the presence of systemStats
  • Memoizes extracted data with appropriate dependencies
  • Returns a consistent interface including translations

Assuming callers provide stable references for extractData and defaultData (as suggested above), this implementation is solid.

view/app/dashboard/page.tsx (4)

27-38: LGTM! Clean integration of the new useDashboard hook.

The destructuring clearly shows all the data, state, and actions needed by the page. This improves readability and addresses the past comment requesting logic be moved out of the page to a hook.


120-133: LGTM! Skeleton loading state provides good UX.

The skeleton implementation correctly handles the loading state before systemStats is available, matching the expected grid layout.


136-179: Well-structured draggable items array.

The dashboardItems array clearly defines each dashboard block with appropriate IDs, components, and grid span classes. The inline containers card definition keeps related logic together.


181-188: LGTM! Clean DraggableGrid integration.

The DraggableGrid usage is straightforward with:

  • Descriptive storageKey for persistence
  • Responsive grid columns
  • resetKey prop to trigger layout reset

This provides a good drag-and-drop UX with automatic persistence.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

♻️ Duplicate comments (3)
view/app/dashboard/page.tsx (2)

98-107: i18n + a11y for Reset Layout button.

-      <Button
+      <Button
         variant="outline"
         size="sm"
         onClick={onResetLayout}
         className="shrink-0"
+        aria-label={t('dashboard.actions.resetLayout')}
       >
         <RefreshCw className="mr-2 h-4 w-4" />
-        Reset Layout
+        {t('dashboard.actions.resetLayout')}
       </Button>

Ensure dashboard.actions.resetLayout exists in locales. Based on learnings.


130-144: i18n the drag hint content.

-          <p className="text-sm font-medium text-foreground">Customize Your Dashboard</p>
+          <p className="text-sm font-medium text-foreground">
+            {t('dashboard.dragHint.title')}
+          </p>
           <p className="text-xs text-muted-foreground mt-1">
-            Hover over any card to see the drag handle on the left. Click and drag to rearrange cards in your preferred order. Your layout will be saved automatically.
+            {t('dashboard.dragHint.body')}
           </p>
@@
-      >
-        Got it
+      >
+        {t('common.gotIt')}
       </Button>

Add keys if missing:

  • dashboard.dragHint.title, dashboard.dragHint.body, common.gotIt. Based on learnings.
view/app/dashboard/components/system/memory-usage.tsx (1)

25-27: Clamp free memory to avoid negative slices.

-  const freeMemory = memory.total - memory.used;
+  const freeMemory = Math.max(0, memory.total - memory.used);
🧹 Nitpick comments (12)
view/app/dashboard/components/system/skeletons/memory-usage.tsx (1)

4-4: Icon mismatch: BarChart used for memory doughnut visualization.

The PR description mentions "Memory Usage card: doughnut chart," but Line 4 imports BarChart and Line 45 uses it as the icon. Consider using a more semantically appropriate icon like PieChart or a memory-related icon (e.g., HardDrive, Database) to match the doughnut chart visualization.

Apply this diff:

-import { BarChart } from 'lucide-react';
+import { PieChart } from 'lucide-react';
       icon={BarChart}
+      icon={PieChart}

Also applies to: 45-45

view/app/dashboard/components/system/skeletons/cpu-usage.tsx (1)

22-22: Consider making skeleton core count configurable or representative.

The skeleton hardcodes 3 cores ([0, 1, 2]), which may not match typical systems (commonly 4, 8, or 16+ cores). Consider using a representative count (e.g., 4 or 6) or making it configurable.

Example adjustment for 4 cores:

-        {[0, 1, 2].map((i) => (
+        {[0, 1, 2, 3].map((i) => (
view/app/dashboard/components/utils/constants.ts (1)

14-16: Optional: Remove redundant as number type annotations.

TypeScript infers numeric literals (e.g., 0) as number by default. The explicit as number annotations on Lines 14-16, 19, 23-25, and 28-30 are redundant and can be simplified for cleaner code.

Example simplification:

   load: {
-    oneMin: 0 as number,
-    fiveMin: 0 as number,
-    fifteenMin: 0 as number
+    oneMin: 0,
+    fiveMin: 0,
+    fifteenMin: 0
   },

Also applies to: 19-19, 23-25, 28-30

view/app/dashboard/components/system/skeletons/disk-usage.tsx (2)

17-19: Progress bar placeholder shows empty.

The progress bar skeleton (Lines 17-19) has no width style on the inner div, so it appears empty. Consider adding a representative width (e.g., style={{ width: '60%' }}) to better preview the actual UI.

       <div className="w-full h-2 bg-gray-200 rounded-full">
-        <div className="h-2 rounded-full bg-gray-400" />
+        <div className="h-2 rounded-full bg-gray-400" style={{ width: '60%' }} />
       </div>

51-66: Consider showing multiple skeleton rows.

The table shows only one skeleton row (Lines 52-65). Showing 2-3 rows would better represent a typical disk usage table with multiple mount points.

           <tbody className="text-xxs sm:text-xs">
-            <tr>
-              <td className="text-left pr-1 sm:pr-2">
-                <Skeleton className="h-3 w-10" />
-              </td>
-              <td className="text-right pr-1 sm:pr-2">
-                <Skeleton className="h-3 w-10" />
-              </td>
-              <td className="text-right pr-1 sm:pr-2">
-                <Skeleton className="h-3 w-10" />
-              </td>
-              <td className="text-right">
-                <Skeleton className="h-3 w-10" />
-              </td>
-            </tr>
+            {[0, 1, 2].map((i) => (
+              <tr key={i}>
+                <td className="text-left pr-1 sm:pr-2">
+                  <Skeleton className="h-3 w-10" />
+                </td>
+                <td className="text-right pr-1 sm:pr-2">
+                  <Skeleton className="h-3 w-10" />
+                </td>
+                <td className="text-right pr-1 sm:pr-2">
+                  <Skeleton className="h-3 w-10" />
+                </td>
+                <td className="text-right">
+                  <Skeleton className="h-3 w-10" />
+                </td>
+              </tr>
+            ))}
           </tbody>
view/app/dashboard/components/system/disk-usage.tsx (1)

49-56: Use translation params instead of manual string replace.

Leverage t(key, { value }) to support languages that don’t place the value last.

-            {t('dashboard.disk.used').replace('{value}', disk.used.toFixed(2))}
+            {t('dashboard.disk.used', { value: disk.used.toFixed(2) })}
@@
-            {t('dashboard.disk.percentage').replace('{value}', formatPercentage(disk.percentage))}
+            {t('dashboard.disk.percentage', { value: formatPercentage(disk.percentage) })}
@@
-            {t('dashboard.disk.total').replace('{value}', disk.total.toFixed(2))}
+            {t('dashboard.disk.total', { value: disk.total.toFixed(2) })}

Ensure these keys accept a "value" param in locales. Based on learnings.

view/app/dashboard/components/system/system-info.tsx (3)

29-47: i18n: replace literals (‘N/A’, ‘cores’) with translatable strings.

Use t(...) so locales can translate and handle pluralization.

-      value: os_type || 'N/A'
+      value: os_type || t('common.notAvailable')
@@
-      value: hostname || 'N/A'
+      value: hostname || t('common.notAvailable')
@@
-      value: cpu_info || 'N/A'
+      value: cpu_info || t('common.notAvailable')
@@
-      value: cpu_cores > 0 ? `${cpu_cores} cores` : 'N/A'
+      value: cpu_cores > 0
+        ? t('dashboard.system.values.cores', { count: String(cpu_cores) })
+        : t('common.notAvailable')
@@
-      value: kernel_version || 'N/A'
+      value: kernel_version || t('common.notAvailable')
@@
-      value: architecture || 'N/A'
+      value: architecture || t('common.notAvailable')

Add the keys if missing:

  • common.notAvailable
  • dashboard.system.values.cores (e.g., "{count} cores"). Based on learnings.

Also applies to: 49-57, 65-67


4-4: Remove unused imports.

Drop unused icons to keep bundle lean.

-import { Server, Cpu, HardDrive, Activity, Terminal, Box, CpuIcon, ScreenShare, ServerCog } from 'lucide-react';
+import { Server, HardDrive, Activity, Terminal, Box, CpuIcon, ScreenShare, ServerCog } from 'lucide-react';

80-83: Prefer stable keys over array index.

Use item.label as key to reduce re-render churn on reorder.

-          {systemInfoItems.map((item, index) => (
-            <div key={index} className="flex items-start gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors">
+          {systemInfoItems.map((item) => (
+            <div key={String(item.label)} className="flex items-start gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors">
view/app/dashboard/components/system/load-average.tsx (1)

42-45: Externalize UI text to i18n.

Axis labels and summary labels should use t(...). Optionally pass localized labels into chart-data helpers.

-            yAxisLabel="Load"
-            xAxisLabel="Time Period"
+            yAxisLabel={t('dashboard.load.yAxis')}
+            xAxisLabel={t('dashboard.load.xAxis')}
@@
-              <TypographyMuted className="text-xs">1 min</TypographyMuted>
+              <TypographyMuted className="text-xs">{t('dashboard.load.oneMin')}</TypographyMuted>
@@
-              <TypographyMuted className="text-xs">5 min</TypographyMuted>
+              <TypographyMuted className="text-xs">{t('dashboard.load.fiveMin')}</TypographyMuted>
@@
-              <TypographyMuted className="text-xs">15 min</TypographyMuted>
+              <TypographyMuted className="text-xs">{t('dashboard.load.fifteenMin')}</TypographyMuted>

Optional follow-up: allow createLoadAverageChartData/config to accept labels from t(...). Based on learnings.

Also applies to: 53-69

view/app/dashboard/components/system/memory-usage.tsx (1)

45-47: Externalize memory labels to i18n.

Replace hardcoded “Used”, “Free”, “Total” with t(...); centerLabel should be localized too.

-              value: `${memory.percentage.toFixed(1)}%`,
-              subLabel: 'Used'
+              value: `${memory.percentage.toFixed(1)}%`,
+              subLabel: t('dashboard.memory.usedShort')
@@
-              <TypographyMuted>
-                Used: {formatGB(memory.used)} GB
-              </TypographyMuted>
+              <TypographyMuted>
+                {t('dashboard.memory.usedWithUnit', { value: formatGB(memory.used) })}
+              </TypographyMuted>
@@
-              <TypographyMuted>
-                Free: {formatGB(freeMemory)} GB
-              </TypographyMuted>
+              <TypographyMuted>
+                {t('dashboard.memory.freeWithUnit', { value: formatGB(freeMemory) })}
+              </TypographyMuted>
@@
-          <TypographyMuted className="text-xs text-center">
-            Total: {formatGB(memory.total)} GB
-          </TypographyMuted>
+          <TypographyMuted className="text-xs text-center">
+            {t('dashboard.memory.totalWithUnit', { value: formatGB(memory.total) })}
+          </TypographyMuted>

Add keys if missing:

  • dashboard.memory.usedShort
  • dashboard.memory.usedWithUnit, freeWithUnit, totalWithUnit (e.g., “Used: {value} GB”). Based on learnings.

Also applies to: 59-67, 71-74

view/app/dashboard/components/system/utils.ts (1)

5-7: Missing unit suffixes in format utilities.

The function names formatGB and formatPercentage suggest they should include unit suffixes, but they only format the numeric value. Consider either:

  1. Appending the unit suffix (recommended): ${value.toFixed(2)} GB and ${value.toFixed(1)}%
  2. Renaming to clarify they only format numbers: formatNumber or toFixedDecimals

Apply this diff to add unit suffixes:

-export const formatGB = (value: number): string => `${value.toFixed(2)}`;
-export const formatPercentage = (value: number): string => `${value.toFixed(1)}`;
+export const formatGB = (value: number): string => `${value.toFixed(2)} GB`;
+export const formatPercentage = (value: number): string => `${value.toFixed(1)}%`;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f04685c and c2c2e7b.

📒 Files selected for processing (17)
  • api/api/versions.json (1 hunks)
  • view/app/dashboard/components/system/cpu-usage.tsx (1 hunks)
  • view/app/dashboard/components/system/disk-usage.tsx (2 hunks)
  • view/app/dashboard/components/system/load-average.tsx (1 hunks)
  • view/app/dashboard/components/system/memory-usage.tsx (1 hunks)
  • view/app/dashboard/components/system/skeletons/cpu-usage.tsx (1 hunks)
  • view/app/dashboard/components/system/skeletons/disk-usage.tsx (1 hunks)
  • view/app/dashboard/components/system/skeletons/index.tsx (1 hunks)
  • view/app/dashboard/components/system/skeletons/load-average.tsx (1 hunks)
  • view/app/dashboard/components/system/skeletons/memory-usage.tsx (1 hunks)
  • view/app/dashboard/components/system/skeletons/system-info.tsx (1 hunks)
  • view/app/dashboard/components/system/system-info.tsx (1 hunks)
  • view/app/dashboard/components/system/system-stats.tsx (2 hunks)
  • view/app/dashboard/components/system/utils.ts (1 hunks)
  • view/app/dashboard/components/utils/constants.ts (1 hunks)
  • view/app/dashboard/hooks/use-dashboard.ts (1 hunks)
  • view/app/dashboard/page.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • view/app/dashboard/hooks/use-dashboard.ts
  • view/app/dashboard/components/system/cpu-usage.tsx
  • api/api/versions.json
🧰 Additional context used
🧬 Code graph analysis (12)
view/app/dashboard/components/system/skeletons/cpu-usage.tsx (4)
view/components/ui/typography.tsx (1)
  • TypographyMuted (114-122)
view/app/dashboard/hooks/use-system-metric.ts (1)
  • useSystemMetric (21-41)
view/app/dashboard/components/utils/constants.ts (1)
  • DEFAULT_METRICS (12-33)
view/app/dashboard/components/system/system-metric-card.tsx (1)
  • SystemMetricCard (16-38)
view/app/dashboard/components/system/skeletons/disk-usage.tsx (5)
view/hooks/use-translation.ts (1)
  • useTranslation (18-74)
view/components/ui/typography.tsx (1)
  • TypographySmall (99-112)
view/app/dashboard/hooks/use-system-metric.ts (1)
  • useSystemMetric (21-41)
view/app/dashboard/components/utils/constants.ts (1)
  • DEFAULT_METRICS (12-33)
view/app/dashboard/components/system/system-metric-card.tsx (1)
  • SystemMetricCard (16-38)
view/app/dashboard/components/system/skeletons/system-info.tsx (3)
view/app/dashboard/components/system/skeletons/index.tsx (1)
  • SystemInfoCardSkeleton (5-5)
view/hooks/use-translation.ts (1)
  • useTranslation (18-74)
view/components/ui/typography.tsx (1)
  • TypographySmall (99-112)
view/app/dashboard/components/system/skeletons/memory-usage.tsx (3)
view/app/dashboard/hooks/use-system-metric.ts (1)
  • useSystemMetric (21-41)
view/app/dashboard/components/utils/constants.ts (1)
  • DEFAULT_METRICS (12-33)
view/app/dashboard/components/system/system-metric-card.tsx (1)
  • SystemMetricCard (16-38)
view/app/dashboard/components/system/system-stats.tsx (1)
view/components/ui/draggable-grid.tsx (2)
  • DraggableItem (26-30)
  • DraggableGrid (93-235)
view/app/dashboard/components/system/system-info.tsx (3)
view/redux/types/monitor.ts (1)
  • SystemStatsType (69-81)
view/hooks/use-translation.ts (1)
  • useTranslation (18-74)
view/app/dashboard/components/system/skeletons/system-info.tsx (1)
  • SystemInfoCardSkeleton (10-36)
view/app/dashboard/components/system/disk-usage.tsx (5)
view/app/dashboard/hooks/use-system-metric.ts (1)
  • useSystemMetric (21-41)
view/app/dashboard/components/utils/constants.ts (1)
  • DEFAULT_METRICS (12-33)
view/app/dashboard/components/system/system-metric-card.tsx (1)
  • SystemMetricCard (16-38)
view/app/dashboard/components/system/skeletons/disk-usage.tsx (1)
  • DiskUsageCardSkeletonContent (12-71)
view/app/dashboard/components/system/utils.ts (1)
  • formatPercentage (7-7)
view/app/dashboard/components/system/memory-usage.tsx (8)
view/redux/types/monitor.ts (1)
  • SystemStatsType (69-81)
view/app/dashboard/hooks/use-system-metric.ts (1)
  • useSystemMetric (21-41)
view/app/dashboard/components/utils/constants.ts (2)
  • DEFAULT_METRICS (12-33)
  • CHART_COLORS (2-9)
view/app/dashboard/components/system/utils.ts (3)
  • createMemoryChartData (47-61)
  • createMemoryChartConfig (63-72)
  • formatGB (6-6)
view/app/dashboard/components/system/system-metric-card.tsx (1)
  • SystemMetricCard (16-38)
view/app/dashboard/components/system/skeletons/memory-usage.tsx (1)
  • MemoryUsageCardSkeletonContent (10-33)
view/components/ui/doughnut-chart-component.tsx (1)
  • DoughnutChartComponent (25-82)
view/components/ui/typography.tsx (1)
  • TypographyMuted (114-122)
view/app/dashboard/components/system/skeletons/load-average.tsx (4)
view/components/ui/typography.tsx (1)
  • TypographyMuted (114-122)
view/app/dashboard/hooks/use-system-metric.ts (1)
  • useSystemMetric (21-41)
view/app/dashboard/components/utils/constants.ts (1)
  • DEFAULT_METRICS (12-33)
view/app/dashboard/components/system/system-metric-card.tsx (1)
  • SystemMetricCard (16-38)
view/app/dashboard/page.tsx (3)
view/app/dashboard/hooks/use-dashboard.ts (1)
  • useDashboard (10-78)
view/app/dashboard/components/smtp-banner.tsx (1)
  • SMTPBanner (13-51)
view/components/ui/draggable-grid.tsx (2)
  • DraggableItem (26-30)
  • DraggableGrid (93-235)
view/app/dashboard/components/system/load-average.tsx (7)
view/redux/types/monitor.ts (1)
  • SystemStatsType (69-81)
view/app/dashboard/hooks/use-system-metric.ts (1)
  • useSystemMetric (21-41)
view/app/dashboard/components/system/utils.ts (2)
  • createLoadAverageChartData (10-30)
  • createLoadAverageChartConfig (32-45)
view/app/dashboard/components/system/system-metric-card.tsx (1)
  • SystemMetricCard (16-38)
view/app/dashboard/components/system/skeletons/load-average.tsx (1)
  • LoadAverageCardSkeletonContent (11-33)
view/components/ui/bar-chart-component.tsx (1)
  • BarChartComponent (22-80)
view/components/ui/typography.tsx (2)
  • TypographyMuted (114-122)
  • TypographySmall (99-112)
view/app/dashboard/components/system/utils.ts (3)
view/components/ui/bar-chart-component.tsx (1)
  • BarChartDataItem (7-11)
view/app/dashboard/components/utils/constants.ts (1)
  • CHART_COLORS (2-9)
view/components/ui/doughnut-chart-component.tsx (1)
  • DoughnutChartDataItem (7-11)
🔇 Additional comments (7)
view/app/dashboard/components/system/system-stats.tsx (1)

31-50: LGTM: Clean draggable grid integration.

The draggable grid implementation is well-structured with proper item IDs and components. The storageKey ensures layout persistence across sessions.

view/app/dashboard/components/system/skeletons/system-info.tsx (1)

10-36: LGTM: Skeleton structure matches system info card.

The skeleton correctly generates 8 placeholder items matching the expanded system info fields (OS, hostname, CPU, cores, memory, kernel, uptime, architecture) with a responsive grid layout.

view/app/dashboard/components/system/skeletons/index.tsx (1)

1-5: LGTM: Clean barrel exports for skeleton components.

All skeleton components are properly re-exported. Note that SystemInfoCardSkeleton doesn't export a separate Content component, which is appropriate given its integrated structure.

view/app/dashboard/components/system/skeletons/load-average.tsx (1)

11-52: LGTM: Load average skeleton structure is appropriate.

The skeleton correctly represents the load average card with a chart placeholder and three standard time intervals (1, 5, 15 minutes). The layout and structure align well with typical load average visualizations.

view/app/dashboard/components/system/disk-usage.tsx (1)

27-31: Verify disk.percentage scale and add scaling if needed.

Code inspection confirms the concern: line 44 uses style={{ width: ${disk.percentage}% }} which assumes a 0–100 scale. If the backend sends 0–1, both the width bar (line 44) and percentage display text (line 52, via formatPercentage) will be incorrect.

The formatPercentage utility does not scale—it only applies .toFixed(1). The DiskStats type specifies percentage: number with no scale documentation.

Verify what scale your backend actually sends in the disk.percentage field. If it's 0–1, multiply by 100 before use at both line 44 and before passing to formatPercentage at line 52.

view/app/dashboard/components/system/utils.ts (2)

1-3: LGTM!

The imports are clean and necessary for the utility functions below.


78-80: Good defensive coding.

The empty array guard prevents runtime errors when no CPU core data is available.

Comment on lines +43 to +46
className={`h-2 rounded-full bg-primary`}
style={{ width: `${disk.percentage}%` }}
/>
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clamp progress width and add a11y on progress container.

Prevent overflow when percentage is out of range and expose progress semantics for screen readers.

-        <div className="w-full h-2 bg-gray-200 rounded-full">
+        <div
+          className="w-full h-2 bg-gray-200 rounded-full"
+          role="progressbar"
+          aria-valuemin={0}
+          aria-valuemax={100}
+          aria-valuenow={Math.round(Math.max(0, Math.min(100, disk.percentage)))}
+          aria-label={t('dashboard.disk.percentageAria')}
+        >
           <div
             className={`h-2 rounded-full bg-primary`}
-            style={{ width: `${disk.percentage}%` }}
+            style={{ width: `${Math.max(0, Math.min(100, disk.percentage))}%` }}
           />
         </div>
📝 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.

Suggested change
className={`h-2 rounded-full bg-primary`}
style={{ width: `${disk.percentage}%` }}
/>
</div>
<div
className="w-full h-2 bg-gray-200 rounded-full"
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(Math.max(0, Math.min(100, disk.percentage)))}
aria-label={t('dashboard.disk.percentageAria')}
>
<div
className={`h-2 rounded-full bg-primary`}
style={{ width: `${Math.max(0, Math.min(100, disk.percentage))}%` }}
/>
</div>
🤖 Prompt for AI Agents
In view/app/dashboard/components/system/disk-usage.tsx around lines 43 to 46,
the progress bar sets width directly from disk.percentage which can overflow if
out of 0–100 range and the container lacks accessibility semantics; clamp the
percentage value to a safe number between 0 and 100 (coerce to Number and
default to 0 if invalid) and use that clamped value for the inline width, and
add ARIA progress semantics to the progress container (role="progressbar",
aria-valuemin="0", aria-valuemax="100", aria-valuenow={clamped} plus an
aria-label or aria-labelledby) so screen readers can read the progress.

Comment on lines +59 to +62
icon: <Activity className="h-4 w-4 text-emerald-500" />,
label: t('dashboard.system.labels.uptime'),
value: load.uptime?.replaceAll(/([hms])(\d)/g, '$1 $2')
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix uptime spacing regex (bug).

Current regex inserts space between unit and next digit; should insert space between digit and unit.

-      value: load.uptime?.replaceAll(/([hms])(\d)/g, '$1 $2')
+      value: load.uptime?.replaceAll(/(\d)([hms])/g, '$1 $2')
🤖 Prompt for AI Agents
In view/app/dashboard/components/system/system-info.tsx around lines 59 to 62,
the uptime spacing regex currently matches a unit then a digit and inserts a
space in the wrong place; change the pattern to match a digit followed by a unit
and replace with digit + space + unit (e.g. use /(\d)([hms])/g with replacement
'$1 $2') so spacing becomes "1 h 2 m" instead of "1h 2m".

Comment on lines 9 to 45
// Chart data transformers
export const createLoadAverageChartData = (load: {
oneMin: number;
fiveMin: number;
fifteenMin: number;
}): BarChartDataItem[] => [
{
name: '1 min',
value: load.oneMin,
fill: CHART_COLORS.blue,
},
{
name: '5 min',
value: load.fiveMin,
fill: CHART_COLORS.green,
},
{
name: '15 min',
value: load.fifteenMin,
fill: CHART_COLORS.orange,
},
];

export const createLoadAverageChartConfig = () => ({
oneMin: {
label: '1 min',
color: CHART_COLORS.blue,
},
fiveMin: {
label: '5 min',
color: CHART_COLORS.green,
},
fifteenMin: {
label: '15 min',
color: CHART_COLORS.orange,
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded labels should be internationalized.

The load average labels ("1 min", "5 min", "15 min") are hardcoded strings, but the PR description states that locale files were updated with translations for new system metadata labels. These user-facing chart labels should use the i18n system to support the English, Spanish, French, Kannada, and Malayalam translations mentioned in the PR.

Consider refactoring these functions to accept translated labels as parameters or use a translation hook/utility:

export const createLoadAverageChartData = (
  load: { oneMin: number; fiveMin: number; fifteenMin: number },
  labels: { oneMin: string; fiveMin: string; fifteenMin: string }
): BarChartDataItem[] => [
  {
    name: labels.oneMin,
    value: load.oneMin,
    fill: CHART_COLORS.blue,
  },
  // ... rest of the items
];
🤖 Prompt for AI Agents
In view/app/dashboard/components/system/utils.ts around lines 9 to 45, the chart
labels "1 min", "5 min", "15 min" are hardcoded; change the API so labels are
provided by the i18n system instead of literal strings: update
createLoadAverageChartData to accept a labels parameter ({ oneMin, fiveMin,
fifteenMin }) and use those for the name fields (preserve types for
BarChartDataItem), and update createLoadAverageChartConfig to accept or return
the same translated labels and colors (or accept a labels parameter) so the
config uses translated label strings; finally, update all call sites to pass
translated strings from the app’s i18n utility/hook (e.g. t('...')) and adjust
types accordingly.

Comment on lines +47 to +72
export const createMemoryChartData = (
used: number,
free: number
): DoughnutChartDataItem[] => [
{
name: 'Used',
value: used,
fill: CHART_COLORS.blue,
},
{
name: 'Free',
value: free,
fill: CHART_COLORS.green,
},
];

export const createMemoryChartConfig = () => ({
used: {
label: 'Used Memory',
color: CHART_COLORS.blue,
},
free: {
label: 'Free Memory',
color: CHART_COLORS.green,
},
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded labels should be internationalized.

Similar to the load average chart, the memory chart labels ("Used", "Free", "Used Memory", "Free Memory") are hardcoded. These should also be internationalized to support the multiple locales mentioned in the PR.

🤖 Prompt for AI Agents
In view/app/dashboard/components/system/utils.ts around lines 47 to 72, the
chart labels "Used", "Free", "Used Memory", and "Free Memory" are hardcoded;
replace them with i18n keys and calls to the project's translation helper (e.g.,
import the existing t/i18n function) so the DoughnutChartDataItem names and the
createMemoryChartConfig labels use translated strings (use clear keys such as
"memory.used", "memory.free", "memory.usedLabel", "memory.freeLabel"), ensure
the import is added at the top, and keep types intact so the chart receives
translated string values at runtime.

Comment on lines +83 to +90
const colors = [
CHART_COLORS.blue,
CHART_COLORS.green,
CHART_COLORS.orange,
CHART_COLORS.purple,
CHART_COLORS.red,
CHART_COLORS.yellow,
];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Extract duplicated color palette to a constant.

The same color array is defined in both createCPUChartData (lines 83-90) and createCPUChartConfig (lines 101-108). This violates the DRY principle and could lead to inconsistencies if colors are updated in only one location.

Apply this diff to extract the palette:

+// CPU color palette
+const CPU_COLORS = [
+  CHART_COLORS.blue,
+  CHART_COLORS.green,
+  CHART_COLORS.orange,
+  CHART_COLORS.purple,
+  CHART_COLORS.red,
+  CHART_COLORS.yellow,
+] as const;
+
 // CPU Chart utilities
 export const createCPUChartData = (
   perCore: Array<{ core_id: number; usage: number }>
 ): BarChartDataItem[] => {
   if (!perCore || perCore.length === 0) {
     return [];
   }
 
-  // Define a color palette for cores (cycle through colors)
-  const colors = [
-    CHART_COLORS.blue,
-    CHART_COLORS.green,
-    CHART_COLORS.orange,
-    CHART_COLORS.purple,
-    CHART_COLORS.red,
-    CHART_COLORS.yellow,
-  ];
-
   return perCore.map((core) => ({
     name: `Core ${core.core_id}`,
     value: core.usage,
-    fill: colors[core.core_id % colors.length],
+    fill: CPU_COLORS[core.core_id % CPU_COLORS.length],
   }));
 };
 
 export const createCPUChartConfig = (coreCount: number) => {
   const config: Record<string, { label: string; color: string }> = {};
-  const colors = [
-    CHART_COLORS.blue,
-    CHART_COLORS.green,
-    CHART_COLORS.orange,
-    CHART_COLORS.purple,
-    CHART_COLORS.red,
-    CHART_COLORS.yellow,
-  ];
 
   for (let i = 0; i < coreCount; i++) {
     config[`core${i}`] = {
       label: `Core ${i}`,
-      color: colors[i % colors.length],
+      color: CPU_COLORS[i % CPU_COLORS.length],
     };
   }
 
   return config;
 };

Also applies to: 101-108

🤖 Prompt for AI Agents
In view/app/dashboard/components/system/utils.ts around lines 83-90 (and also
101-108), the CHART_COLORS array is duplicated between createCPUChartData and
createCPUChartConfig; extract the palette to a single top-level constant (e.g.,
const CPU_CHART_PALETTE = [CHART_COLORS.blue, CHART_COLORS.green,
CHART_COLORS.orange, CHART_COLORS.purple, CHART_COLORS.red, CHART_COLORS.yellow]
placed near other module-level constants), mark it readonly if applicable, and
replace the inline arrays in both functions to reference CPU_CHART_PALETTE so
both use the same source of truth.

];

return perCore.map((core) => ({
name: `Core ${core.core_id}`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded "Core" prefix should be internationalized.

The "Core" prefix in the CPU chart labels is hardcoded and appears in user-facing charts. Like the other chart labels, this should be internationalized to support the multiple locales mentioned in the PR.

Consider accepting a translated label prefix as a parameter:

export const createCPUChartData = (
  perCore: Array<{ core_id: number; usage: number }>,
  coreLabel: string = 'Core' // default for backward compatibility
): BarChartDataItem[] => {
  // ...
  return perCore.map((core) => ({
    name: `${coreLabel} ${core.core_id}`,
    value: core.usage,
    fill: CPU_COLORS[core.core_id % CPU_COLORS.length],
  }));
};

Also applies to: 112-112

🤖 Prompt for AI Agents
In view/app/dashboard/components/system/utils.ts around lines 93 and 112, the
CPU chart label currently hardcodes the "Core" prefix; change the
createCPUChartData function to accept a coreLabel parameter (defaulting to
'Core' for backward compatibility) and use that parameter when building the name
(`${coreLabel} ${core.core_id}`), update both occurrences at lines ~93 and ~112,
and update any callers to pass a translated label from the i18n layer (e.g.,
t('coreLabel')) so charts are localized while keeping the default behavior for
callers that don't provide the label.

percentage: 0 as number,
used: 0 as number,
total: 0 as number,
allMounts: [] as any[]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Weak typing: allMounts uses any[].

Line 31 uses any[] for allMounts, which bypasses type safety. Define a proper type for mount entries based on the actual structure (e.g., { mountPoint: string; total: number; used: number; percentage: number }[]).

Example fix:

+interface MountInfo {
+  mountPoint: string;
+  total: number;
+  used: number;
+  percentage: number;
+}
+
 export const DEFAULT_METRICS = {
   // ...
   disk: {
     percentage: 0 as number,
     used: 0 as number,
     total: 0 as number,
-    allMounts: [] as any[]
+    allMounts: [] as MountInfo[]
   }
 };

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In view/app/dashboard/components/utils/constants.ts around line 31, replace the
weak any[] typing for allMounts with a concrete mount-entry type (for example
export an interface/type like { mountPoint: string; total: number; used: number;
percentage: number } and then declare allMounts: MountEntry[]). Update the
declaration to use the new type, export the type if reused elsewhere, and fix
any places that read/write allMounts to conform to the fields and types (adjust
parsing/initialization where necessary).

Comment on lines 153 to 165
const MonitoringSection = ({
systemStats,
containersData,
t
t,
layoutResetKey,
onLayoutChange
}: {
systemStats: any;
containersData: any;
t: any;
layoutResetKey: number;
onLayoutChange: () => void;
}) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Replace any in MonitoringSection props with concrete types.

Tighten types for safer refactors and editor tooling.

-import { useDashboard } from './hooks/use-dashboard';
+import { useDashboard } from './hooks/use-dashboard';
+import type { SystemStatsType } from '@/redux/types/monitor';
+// TODO: replace `unknown[]` with your concrete container type (e.g., ContainerData[])
+
@@
-}: {
-  systemStats: any;
-  containersData: any;
-  t: any;
-  layoutResetKey: number;
-  onLayoutChange: () => void;
-}) => {
+}: {
+  systemStats: SystemStatsType | null;
+  containersData: unknown[];
+  t: (key: string) => string;
+  layoutResetKey: number;
+  onLayoutChange: () => void;
+}) => {

To locate an existing container type, run:


🏁 Script executed:

#!/bin/bash
# Find candidate container types/interfaces in the repo
rg -nP --type=ts -C3 '\b(interface|type)\s+Container\w*'

Length of output: 2484


I need to directly inspect the codebase to verify the suggested types. Let me check the monitor.ts file and how MonitoringSection is used.

#!/bin/bash
# Check contents of monitor.ts to verify SystemStatsType exists
echo "=== Contents of view/redux/types/monitor.ts ==="
cat view/redux/types/monitor.ts

echo -e "\n=== MonitoringSection definition in dashboard page ==="
# Find and extract the MonitoringSection definition
rg -n 'const MonitoringSection' view/app/dashboard/page.tsx -A 30

echo -e "\n=== Check imports in dashboard page ==="
head -30 view/app/dashboard/page.tsx

🏁 Script executed:

#!/bin/bash
# Check contents of monitor.ts to verify SystemStatsType exists
echo "=== Contents of view/redux/types/monitor.ts ==="
cat view/redux/types/monitor.ts

echo -e "\n=== MonitoringSection definition in dashboard page ==="
# Find MonitoringSection definition and its surroundings
rg -n 'const MonitoringSection' view/app/dashboard/page.tsx -A 25

echo -e "\n=== Check how MonitoringSection is called ==="
rg -n '<MonitoringSection' view/app/dashboard/page.tsx -B 5 -A 5

echo -e "\n=== Check imports in dashboard page (first 40 lines) ==="
head -40 view/app/dashboard/page.tsx

Length of output: 4554


🏁 Script executed:

#!/bin/bash
# Check use-dashboard hook return types
echo "=== use-dashboard hook (check file location and content) ==="
find view/app/dashboard -name "*use-dashboard*" -type f

echo -e "\n=== use-dashboard hook content ==="
rg -n 'useDashboard|systemStats|containersData' view/app/dashboard/hooks/use-dashboard.ts -A 3 -B 1 | head -80

echo -e "\n=== use-translation hook content ==="
rg -n 'export.*useTranslation|return.*{.*t' view/hooks/use-translation.ts -A 5 -B 2 | head -60

echo -e "\n=== Check return type of useDashboard ==="
rg -n 'return.*{' view/app/dashboard/hooks/use-dashboard.ts -A 15 | tail -30

Length of output: 923


🏁 Script executed:

#!/bin/bash
# Check useMonitor hook
echo "=== useMonitor hook location ==="
find view -name "*use-monitor*" -type f 2>/dev/null

echo -e "\n=== useMonitor hook definition ==="
rg -n 'export.*useMonitor' view -A 30 --type=ts

echo -e "\n=== useTranslation hook definition ==="
cat view/hooks/use-translation.ts

echo -e "\n=== Check types returned from useMonitor ==="
rg -n 'systemStats\|containersData' view/hooks/use-monitor.ts -B 2 -A 2

Length of output: 2342


🏁 Script executed:

#!/bin/bash
echo "=== useMonitor hook definition ==="
cat view/app/dashboard/hooks/use-monitor.ts

Length of output: 3002


Use concrete types: ContainerData[] for containers and stronger typing for the t function.

The suggested types are partially correct but need refinement. Based on verification:

  1. systemStats: SystemStatsType | null — ✅ Correct, matches useMonitor() return type exactly
  2. containersData: unknown[] — ✅ Should be ContainerData[] instead. The ContainerData type is already exported from @/redux/types/monitor and is what useMonitor() actually returns (useState<ContainerData[]>), so use it directly
  3. t: (key: string) => string — Acceptable but incomplete. The actual signature from useTranslation() is (key: translationKey, params?: Record<string, string>): string for better editor autocompletion and type safety

Update the suggested diff to use ContainerData[] directly instead of unknown[], removing the TODO comment:

-}: {
-  systemStats: any;
-  containersData: any;
-  t: any;
-  layoutResetKey: number;
-  onLayoutChange: () => void;
-}) => {
+}: {
+  systemStats: SystemStatsType | null;
+  containersData: ContainerData[];
+  t: (key: translationKey, params?: Record<string, string>) => string;
+  layoutResetKey: number;
+  onLayoutChange: () => void;
+}) => {

Add imports:

import type { SystemStatsType, ContainerData } from '@/redux/types/monitor';
import type { translationKey } from '@/hooks/use-translation';
🤖 Prompt for AI Agents
In view/app/dashboard/page.tsx around lines 153 to 165, replace the loose
any/unknown types and TODO with concrete imports and types: import type {
SystemStatsType, ContainerData } from '@/redux/types/monitor' and import type {
translationKey } from '@/hooks/use-translation', change the prop types to
systemStats: SystemStatsType | null, containersData: ContainerData[], and t:
(key: translationKey, params?: Record<string,string>) => string, and remove the
TODO comment; ensure you update the function signature and add the new type-only
imports at the top of the file.

@raghavyuva raghavyuva merged commit e13c24a into master Oct 27, 2025
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants