Skip to content

Add pause/resume monitoring feature#27

Draft
jbingham17 wants to merge 3 commits intomainfrom
feature/pause-monitoring
Draft

Add pause/resume monitoring feature#27
jbingham17 wants to merge 3 commits intomainfrom
feature/pause-monitoring

Conversation

@jbingham17
Copy link
Copy Markdown
Contributor

@jbingham17 jbingham17 commented Mar 16, 2026

Summary

  • Adds a pause/resume feature for metric updates, toggled via the P keyboard shortcut or a new pause button in the status bar
  • Displays a blinking PAUSED indicator in the header when monitoring is paused
  • Disables the refresh rate selector while paused to avoid confusion

Test plan

  • Press P to pause — verify metrics stop updating and "PAUSED" indicator appears
  • Press P again to resume — verify metrics resume and indicator disappears
  • Click the pause button in the status bar — verify same behavior
  • Confirm P key does not trigger while typing in the filter input
  • Verify refresh rate dropdown is disabled while paused

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a Navbar to the app layout.
    • Pause control (button + P key) to suspend metrics polling; "PAUSED" indicator with blinking animation.
    • Persisted filter state in local storage.
  • Behavior Changes

    • Refresh-rate selector disabled while paused.
    • Filter matching improved (case-insensitive, includes PID); sorting comparisons refined.
    • Metrics fetcher supports API key and automatic retry/backoff.
  • Style

    • New header, navbar, and pause-button visuals with hover/active/paused states.

Press P to pause/resume metric updates. Adds a pause button to the
status bar, a blinking PAUSED indicator in the header, and disables
the refresh rate selector while paused.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 16, 2026

📝 Walkthrough

Walkthrough

Adds a global pause feature (P key + UI) that stops metrics polling, admin detection in Header, a new Navbar, StatusBar localStorage persistence for filters, useSystemMetrics API key + retry/backoff logic, and non-memoized filtering/sorting changes in ProcessTable. CSS adds navbar/header/pause UI styles.

Changes

Cohort / File(s) Summary
Styling
src/App.css
Adds .navbar*, .header*, .pause-button / .pause-button.paused, .paused-indicator and @keyframes blink (+135 lines): new layout and pause-related UI styles.
App State & Layout
src/App.tsx
Adds paused state, P-key toggle (keydown handler via useCallback/useEffect), renders Navbar, and derives refreshRate=0 when paused to stop polling.
Header
src/components/Header.tsx
HeaderProps gains optional paused?: boolean; Header fetches admin status (/api/auth/me) into adminMode and conditionally renders ADMIN and PAUSED indicators.
StatusBar
src/components/StatusBar.tsx
Adds paused?: boolean and onPauseToggle?: () => void; disables refresh-rate select when paused; adds pause button; persists filter state (filter, last_active, user_agent) to localStorage on change.
Navbar
src/components/Navbar...
New Navbar component introduced and imported in App (layout integration).
Metrics Hook
src/hooks/useSystemMetrics.ts
Adds VITE_BTOP_API_KEY support (Authorization header), introduces retry/backoff (MAX_RETRIES, BASE_DELAY, MAX_DELAY, retryCount) with exponential backoff on fetch errors, and early-return behavior when refreshRate is 0 to suspend polling.
Process Table
src/components/ProcessTable.tsx
Removed useMemo; filtering/sorting now computed every render. PID filtering uses case-insensitive numeric/string handling; string comparisons use localeCompare. Logic moved from memoized path to direct computation.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant App as App (paused state)
    participant StatusBar as StatusBar
    participant Header as Header
    participant Metrics as useSystemMetrics
    participant API as Metrics API

    User->>App: Press "P" key / Click pause button
    App->>App: toggle paused state
    App->>StatusBar: pass paused prop & onPauseToggle
    App->>Header: pass paused prop
    StatusBar-->>App: onPauseToggle callback (if clicked)
    App->>Metrics: derive refreshRate (0 when paused)
    Metrics->>Metrics: if refreshRate == 0 -> clear interval / skip scheduled fetches
    Metrics->>API: (when not paused) fetch metrics (with optional Authorization)
    API-->>Metrics: metrics response / error
    Metrics->>Metrics: on error -> schedule retry with exponential backoff
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Min-Level ❌ Error PR introduces critical API key exposure in client bundle and major user fingerprinting privacy violations. Remove VITE_ prefix for API key or move to server-side endpoint. Remove localStorage storage of user agent and activity timestamps.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add pause/resume monitoring feature' clearly and concisely summarizes the primary objective of the pull request: enabling users to pause and resume system metric monitoring via keyboard shortcut and UI button.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/pause-monitoring
📝 Coding Plan
  • Generate coding plan for human review comments

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
Copy Markdown

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

🧹 Nitpick comments (4)
src/App.tsx (1)

93-94: Minor: Consider extracting onPauseToggle to a stable callback.

The inline arrow function () => setPaused(prev => !prev) creates a new function reference on every render, which could cause unnecessary re-renders of StatusBar if it's memoized.

♻️ Optional: Use stable callback reference
+  const togglePause = useCallback(() => setPaused(prev => !prev), []);
+
   // ... later in JSX
   <StatusBar
     filter={filter}
     onFilterChange={setFilter}
     refreshRate={refreshRate}
     onRefreshRateChange={setRefreshRate}
     paused={paused}
-    onPauseToggle={() => setPaused(prev => !prev)}
+    onPauseToggle={togglePause}
   />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 93 - 94, Extract the inline toggle into a stable
callback to avoid creating a new function each render: create a memoized handler
(e.g., togglePause) using useCallback that calls setPaused(prev => !prev) and
pass that handler to the StatusBar onPauseToggle prop instead of the inline
arrow; reference the StatusBar prop onPauseToggle and the state setter setPaused
in App.tsx when making this change.
src/App.css (1)

495-506: Consider respecting reduced motion preferences.

The blinking animation on the paused indicator is subtle, but users with vestibular disorders or motion sensitivity may prefer it disabled. Consider adding a prefers-reduced-motion media query.

♿ Optional: Respect reduced motion preferences
 `@keyframes` blink {
   0%, 100% { opacity: 1; }
   50% { opacity: 0.4; }
 }
+
+@media (prefers-reduced-motion: reduce) {
+  .paused-indicator {
+    animation: none;
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.css` around lines 495 - 506, The .paused-indicator animation
currently always blinks via the `@keyframes` blink; update the CSS to respect
users' reduced motion preferences by adding a prefers-reduced-motion media query
that disables the blinking (e.g., set animation: none or remove animation for
.paused-indicator) and keep the existing `@keyframes` blink for environments that
allow motion; reference .paused-indicator and `@keyframes` blink when making the
change.
src/components/StatusBar.tsx (1)

47-53: Accessibility consideration for pause button.

The pause button uses Unicode symbols ( and ) which may render inconsistently across platforms/browsers or be problematic for screen readers. Consider adding an aria-label for accessibility.

Also, if onPauseToggle is undefined, clicking the button does nothing silently. While this is safe, it may be worth guarding if the button shouldn't be rendered without a handler.

♿ Suggested accessibility improvement
        <button
          className={`pause-button ${paused ? 'paused' : ''}`}
          onClick={onPauseToggle}
          title={paused ? 'Resume monitoring (P)' : 'Pause monitoring (P)'}
+         aria-label={paused ? 'Resume monitoring' : 'Pause monitoring'}
+         aria-pressed={paused}
        >
          {paused ? '▶' : '⏸'}
        </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/StatusBar.tsx` around lines 47 - 53, The pause button in
StatusBar.tsx should be made accessible and robust: add a clear aria-label that
changes with the paused state (e.g., aria-label={paused ? 'Resume monitoring' :
'Pause monitoring'}) so screen readers don't rely on Unicode glyphs, and guard
the handler by either not rendering the button or rendering it disabled when
onPauseToggle is undefined (e.g., check onPauseToggle before rendering or set
disabled and aria-disabled) so clicking does not silently no-op; update
references to the pause-button class and the paused prop accordingly.
src/hooks/useSystemMetrics.ts (1)

35-35: Consider the loading state when paused before initial fetch.

If the hook is called with refreshRate === 0 from the start (or transitions to paused before the first fetch completes), the loading state will remain true indefinitely since fetchMetrics is never called and setLoading(false) is only invoked within fetchMetrics.

In the current App.tsx implementation, paused defaults to false, so this isn't immediately problematic. However, if the initial state or behavior changes in the future, this could cause the app to be stuck in a loading state.

💡 Optional: Handle loading state explicitly when paused
  useEffect(() => {
-   if (refreshRate === 0) return; // paused
+   if (refreshRate === 0) {
+     setLoading(false); // Not loading when intentionally paused
+     return;
+   }
    fetchMetrics();
    const interval = setInterval(fetchMetrics, refreshRate);
    return () => clearInterval(interval);
  }, [fetchMetrics, refreshRate]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useSystemMetrics.ts` at line 35, In useSystemMetrics, the early
return when refreshRate === 0 can leave loading stuck true because fetchMetrics
(which sets loading false) never runs; update the logic so that when you
short-circuit due to refreshRate === 0 you explicitly clear the loading state
(call setLoading(false) or otherwise mark loading complete) or ensure an initial
fetch runs regardless of refreshRate; specifically modify the branch around "if
(refreshRate === 0) return" to either invoke fetchMetrics() once or call
setLoading(false) so that the loading state controlled in this hook (loading,
setLoading) and the fetchMetrics function no longer remain stuck when paused.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/App.css`:
- Around line 495-506: The .paused-indicator animation currently always blinks
via the `@keyframes` blink; update the CSS to respect users' reduced motion
preferences by adding a prefers-reduced-motion media query that disables the
blinking (e.g., set animation: none or remove animation for .paused-indicator)
and keep the existing `@keyframes` blink for environments that allow motion;
reference .paused-indicator and `@keyframes` blink when making the change.

In `@src/App.tsx`:
- Around line 93-94: Extract the inline toggle into a stable callback to avoid
creating a new function each render: create a memoized handler (e.g.,
togglePause) using useCallback that calls setPaused(prev => !prev) and pass that
handler to the StatusBar onPauseToggle prop instead of the inline arrow;
reference the StatusBar prop onPauseToggle and the state setter setPaused in
App.tsx when making this change.

In `@src/components/StatusBar.tsx`:
- Around line 47-53: The pause button in StatusBar.tsx should be made accessible
and robust: add a clear aria-label that changes with the paused state (e.g.,
aria-label={paused ? 'Resume monitoring' : 'Pause monitoring'}) so screen
readers don't rely on Unicode glyphs, and guard the handler by either not
rendering the button or rendering it disabled when onPauseToggle is undefined
(e.g., check onPauseToggle before rendering or set disabled and aria-disabled)
so clicking does not silently no-op; update references to the pause-button class
and the paused prop accordingly.

In `@src/hooks/useSystemMetrics.ts`:
- Line 35: In useSystemMetrics, the early return when refreshRate === 0 can
leave loading stuck true because fetchMetrics (which sets loading false) never
runs; update the logic so that when you short-circuit due to refreshRate === 0
you explicitly clear the loading state (call setLoading(false) or otherwise mark
loading complete) or ensure an initial fetch runs regardless of refreshRate;
specifically modify the branch around "if (refreshRate === 0) return" to either
invoke fetchMetrics() once or call setLoading(false) so that the loading state
controlled in this hook (loading, setLoading) and the fetchMetrics function no
longer remain stuck when paused.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a8ee7141-dac9-46da-8aa3-a7aeab95f0cf

📥 Commits

Reviewing files that changed from the base of the PR and between 6cfe335 and dcf0093.

📒 Files selected for processing (5)
  • src/App.css
  • src/App.tsx
  • src/components/Header.tsx
  • src/components/StatusBar.tsx
  • src/hooks/useSystemMetrics.ts
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-02-18T05:07:00.671Z
Learnt from: jbingham17
Repo: coderabbit-demo/btop PR: 8
File: src/types.ts:32-32
Timestamp: 2026-02-18T05:07:00.671Z
Learning: In the coderabbit-demo/btop repository, avoid inline comments on TypeScript interface properties. Ensure property types are self-documenting through clear names and interface structure. If a property requires explanation, document it above the interface or with JSDoc comments for the property, not inline on the same line.

Applied to files:

  • src/hooks/useSystemMetrics.ts
🔇 Additional comments (2)
src/components/Header.tsx (1)

10-10: LGTM!

Clean implementation of the paused indicator. The optional prop maintains backward compatibility, and the conditional rendering is straightforward. The placement in header-right provides good visibility alongside load and task metrics.

Also applies to: 26-26, 58-58

src/App.tsx (1)

14-27: Well-implemented pause state management.

The implementation correctly:

  • Guards against triggering pause while typing in inputs (Line 18)
  • Handles both p and P keys (Line 19)
  • Uses useCallback for stable event handler reference
  • Properly cleans up the event listener on unmount

@JDCR-GH
Copy link
Copy Markdown

JDCR-GH commented Mar 16, 2026

@coderabbitai generate unit tests

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 16, 2026

CodeRabbit chat interactions are restricted to organization members for this repository. Ask an organization member to interact with CodeRabbit, or set chat.allow_non_org_members: true in your configuration.

@jbingham17
Copy link
Copy Markdown
Contributor Author

@coderabbitai generate unit tests

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 16, 2026

✅ Unit tests generated successfully!

Generated 4 test files. The changes have been created in this pull request: View PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 16, 2026

✅ Actions performed

Unit Test Generation started...


Unit tests generated successfully.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 16, 2026

✅ Actions performed

Unit Test Generation started...

@coderabbit-demo coderabbit-demo deleted a comment from coderabbitai bot Mar 16, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

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

🧹 Nitpick comments (4)
src/App.tsx (1)

18-23: Consider adding HTMLTextAreaElement to the input guard.

The check correctly excludes HTMLInputElement and HTMLSelectElement, but if a textarea is ever added to the UI, pressing P while typing there would toggle the pause state. This is a defensive improvement.

🛡️ Suggested defensive improvement
 const handleKeyDown = useCallback((e: KeyboardEvent) => {
-  if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement) return;
+  if (
+    e.target instanceof HTMLInputElement ||
+    e.target instanceof HTMLSelectElement ||
+    e.target instanceof HTMLTextAreaElement
+  ) return;
   if (e.key === 'p' || e.key === 'P') {
     setPaused(prev => !prev);
   }
 }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 18 - 23, The keydown handler handleKeyDown
currently ignores events from HTMLInputElement and HTMLSelectElement but not
from textareas; update the guard to also return early when e.target is an
instance of HTMLTextAreaElement so typing in a textarea doesn't toggle pause,
i.e., add HTMLTextAreaElement to the input-check alongside HTMLInputElement and
HTMLSelectElement before the e.key === 'p' check that calls setPaused.
src/App.css (3)

573-593: Add :focus-visible state for keyboard accessibility.

Similar to .nav-item, the pause button needs a visible focus indicator for keyboard users.

♿ Suggested focus styles
 .pause-button:hover {
   border-color: var(--color-cyan);
   box-shadow: 0 0 10px rgba(34, 211, 238, 0.2);
 }
+
+.pause-button:focus-visible {
+  outline: 2px solid var(--color-cyan);
+  outline-offset: 2px;
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.css` around lines 573 - 593, The pause button lacks a
keyboard-visible focus state; add a .pause-button:focus-visible rule (and
.pause-button.paused:focus-visible) that applies a clear focus indicator
consistent with .nav-item (e.g., a visible outline or box-shadow using
var(--color-cyan) for normal and var(--color-yellow) for paused) and ensure it
does not rely solely on :hover so keyboard users see the same emphasis as mouse
users.

169-195: Consider adding :focus-visible styles for keyboard accessibility.

The .nav-item buttons have hover states but lack visible focus indicators for keyboard navigation. Users navigating with Tab won't see which item is focused.

♿ Suggested focus styles
 .nav-item:hover {
   background: var(--bg-tertiary);
   color: var(--color-text);
   border-color: var(--border-color);
 }
+
+.nav-item:focus-visible {
+  outline: 2px solid var(--color-cyan);
+  outline-offset: 2px;
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.css` around lines 169 - 195, The .nav-item rules lack visible
keyboard focus styling; add a :focus-visible selector for .nav-item to provide a
clear high-contrast outline and (optionally) matching background/border changes
for keyboard users—e.g. implement .nav-item:focus-visible to set outline (or
box-shadow), outline-offset, and color/border adjustments similar to
.nav-item:hover/.nav-item.active so focused items are clearly visible when
tabbed to. Ensure the focus style does not rely on hover and uses accessible
contrast values consistent with --color-cyan/--border-color variables.

595-606: Add prefers-reduced-motion support for the blinking animation.

Users with vestibular disorders or motion sensitivity may find continuous blinking animations uncomfortable. The prefers-reduced-motion media query allows respecting user preferences.

♿ Suggested reduced-motion support
 `@keyframes` blink {
   0%, 100% { opacity: 1; }
   50% { opacity: 0.4; }
 }
+
+@media (prefers-reduced-motion: reduce) {
+  .paused-indicator {
+    animation: none;
+  }
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.css` around lines 595 - 606, The blinking animation on
.paused-indicator and the `@keyframes` blink does not respect users'
prefers-reduced-motion settings; update the CSS so the animation only runs when
prefers-reduced-motion is not requested (e.g., wrap the animation assignment in
`@media` (prefers-reduced-motion: no-preference) or conditionally apply animation
there) and add an explicit override for prefers-reduced-motion: reduce to set
.paused-indicator animation to none (or equivalent static styling) so the
element remains visible without motion for users who opt out.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/App.css`:
- Around line 573-593: The pause button lacks a keyboard-visible focus state;
add a .pause-button:focus-visible rule (and .pause-button.paused:focus-visible)
that applies a clear focus indicator consistent with .nav-item (e.g., a visible
outline or box-shadow using var(--color-cyan) for normal and var(--color-yellow)
for paused) and ensure it does not rely solely on :hover so keyboard users see
the same emphasis as mouse users.
- Around line 169-195: The .nav-item rules lack visible keyboard focus styling;
add a :focus-visible selector for .nav-item to provide a clear high-contrast
outline and (optionally) matching background/border changes for keyboard
users—e.g. implement .nav-item:focus-visible to set outline (or box-shadow),
outline-offset, and color/border adjustments similar to
.nav-item:hover/.nav-item.active so focused items are clearly visible when
tabbed to. Ensure the focus style does not rely on hover and uses accessible
contrast values consistent with --color-cyan/--border-color variables.
- Around line 595-606: The blinking animation on .paused-indicator and the
`@keyframes` blink does not respect users' prefers-reduced-motion settings; update
the CSS so the animation only runs when prefers-reduced-motion is not requested
(e.g., wrap the animation assignment in `@media` (prefers-reduced-motion:
no-preference) or conditionally apply animation there) and add an explicit
override for prefers-reduced-motion: reduce to set .paused-indicator animation
to none (or equivalent static styling) so the element remains visible without
motion for users who opt out.

In `@src/App.tsx`:
- Around line 18-23: The keydown handler handleKeyDown currently ignores events
from HTMLInputElement and HTMLSelectElement but not from textareas; update the
guard to also return early when e.target is an instance of HTMLTextAreaElement
so typing in a textarea doesn't toggle pause, i.e., add HTMLTextAreaElement to
the input-check alongside HTMLInputElement and HTMLSelectElement before the
e.key === 'p' check that calls setPaused.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 51a66f6a-b21b-49d7-ae65-657d04743122

📥 Commits

Reviewing files that changed from the base of the PR and between dcf0093 and 54bc010.

📒 Files selected for processing (2)
  • src/App.css
  • src/App.tsx
📜 Review details
🔇 Additional comments (4)
src/App.tsx (4)

1-2: LGTM!

The imports are correctly updated to include the necessary React hooks (useEffect, useCallback) for the keyboard event handling, and the new Navbar component import is properly placed.


15-16: LGTM!

Clean implementation of the pause state. Using 0 as the refresh rate to signal paused state to useSystemMetrics is a reasonable approach that avoids adding a separate paused parameter to the hook.


25-28: LGTM!

The useEffect correctly manages the global keydown listener lifecycle with proper cleanup. The dependency on handleKeyDown ensures the listener stays in sync if the callback ever changes.


90-97: LGTM!

The paused state and onPauseToggle callback are correctly wired to StatusBar. While the inline arrow function creates a new reference on each render, the performance impact is negligible here since StatusBar is unlikely to be a heavily optimized component.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@jbingham17 jbingham17 marked this pull request as draft March 19, 2026 00:44
Copy link
Copy Markdown

@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

🧹 Nitpick comments (2)
src/components/ProcessTable.tsx (1)

23-74: Consider restoring useMemo for filtering and sorting.

The filtering and sorting logic now runs on every render, even when processes, filter, sortField, and sortDirection haven't changed. For a system monitoring app with potentially hundreds of processes, this can cause unnecessary computation—especially when the user selects a row (updating selectedPid) or when the app is paused and no data changes.

♻️ Proposed refactor to restore memoization
-import { useState } from 'react';
+import { useState, useMemo } from 'react';

Then wrap the filtering and sorting logic:

-  // Filtering and sorting on every render
-  let filtered = processes;
-
-  if (filter) {
-    const lowerFilter = filter.toLowerCase();
-    filtered = processes.filter(
-      p =>
-        p.command.toLowerCase().includes(lowerFilter) ||
-        p.user.toLowerCase().includes(lowerFilter) ||
-        p.pid.toString().includes(lowerFilter)
-    );
-  }
-
-  const filteredAndSortedProcesses = [...filtered].sort((a, b) => {
+  const filteredAndSortedProcesses = useMemo(() => {
+    let filtered = processes;
+
+    if (filter) {
+      const lowerFilter = filter.toLowerCase();
+      filtered = processes.filter(
+        p =>
+          p.command.toLowerCase().includes(lowerFilter) ||
+          p.user.toLowerCase().includes(lowerFilter) ||
+          p.pid.toString().includes(lowerFilter)
+      );
+    }
+
+    return [...filtered].sort((a, b) => {
       let aVal: number | string;
       let bVal: number | string;
       // ... sorting logic unchanged ...
-  });
+    });
+  }, [processes, filter, sortField, sortDirection]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ProcessTable.tsx` around lines 23 - 74, Filtering and sorting
are recomputed on every render causing wasted work; wrap the logic that produces
filteredAndSortedProcesses in React's useMemo and depend on [processes, filter,
sortField, sortDirection] so the computation only runs when inputs change;
update the block that computes filtered, lowerFilter and the sort switch (the
code that currently assigns filtered and computes filteredAndSortedProcesses) to
be returned from a useMemo callback and keep existing variable names
(filteredAndSortedProcesses, filter, sortField, sortDirection, processes) so
selectors and consumers remain unchanged.
src/components/Header.tsx (1)

69-69: Admin mode feature is not described in PR objectives.

The commit message mentions "Add admin mode" but the PR description only covers pause/resume functionality. This appears to be scope creep. Consider splitting into a separate PR or updating the PR description to document this feature.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Header.tsx` at line 69, The PR introduces an "admin mode" UI
change (the conditional span using adminMode in Header.tsx) that isn't described
in the PR description; either remove or isolate this change from the current PR
or update the PR description to document the new feature. Locate the conditional
JSX reference to adminMode in the Header component (the line rendering <span
...>ADMIN</span>), and then either (A) move that change to a separate branch/PR
focused on admin mode, or (B) keep it here but expand the PR description to
explain the admin mode purpose, behavior, and any related files touched so
reviewers can assess scope.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/Header.tsx`:
- Around line 38-46: The useEffect in Header.tsx calls fetch('/api/auth/me')
which will always 404 because the server only exposes /api/metrics, /api/health
and /api/environment; either implement the missing auth endpoint on the server
(add a handler that returns { isAdmin: boolean } for GET /api/auth/me) or remove
the client-side code that references fetch('/api/auth/me')/useEffect and the
setAdminMode/adminMode logic until auth exists; locate the useEffect block in
Header.tsx (the fetch('/api/auth/me') call, useEffect, setAdminMode and
adminMode) and either add a corresponding server route that returns the expected
JSON or delete/guard the client code to avoid the dead fetch.
- Around line 40-45: The fetch to '/api/auth/me' in the Header component should
use an AbortController for cleanup and robustly handle non-JSON responses:
create an AbortController, pass its signal to fetch, and on cleanup call
controller.abort(); after fetch, if !res.ok throw or set admin false, and before
calling res.json() verify res.headers.get('content-type') includes
'application/json' (or wrap res.json() in try/catch) so a plain-text 404 doesn't
throw; catch all errors (including AbortError) and call setAdminMode(false) only
for non-abort failures or simply ensure aborts don't trigger state updates.

In `@src/components/StatusBar.tsx`:
- Around line 22-28: Remove unnecessary user-agent and timestamp persistence
from handleFilterChange: only persist the filter value to localStorage under the
btop_filter key and call onFilterChange(value); delete the localStorage.setItem
calls for btop_user_agent and btop_last_active to avoid collecting user-agent
and activity timestamp data (remove references to btop_user_agent and
btop_last_active in handleFilterChange and any related code that depends on
them).

In `@src/hooks/useSystemMetrics.ts`:
- Line 22: The retry logic in useSystemMetrics uses state (retryCount /
setRetryCount) causing stale closures and no cleanup; replace retryCount state
with a useRef (e.g., retryCountRef) and store the active timeout id in another
ref (e.g., retryTimeoutRef), update retryCountRef.current instead of
setRetryCount, schedule retries with setTimeout using the refs so fetchMetrics
closure is stable, and add cleanup in the effect/unmount to
clearTimeout(retryTimeoutRef.current) and to cancel retries when monitoring is
paused (check the paused flag before scheduling). Ensure fetchMetrics reads
retryCountRef.current and that all timeouts are cleared on pause/unmount to
prevent continued retries.

---

Nitpick comments:
In `@src/components/Header.tsx`:
- Line 69: The PR introduces an "admin mode" UI change (the conditional span
using adminMode in Header.tsx) that isn't described in the PR description;
either remove or isolate this change from the current PR or update the PR
description to document the new feature. Locate the conditional JSX reference to
adminMode in the Header component (the line rendering <span ...>ADMIN</span>),
and then either (A) move that change to a separate branch/PR focused on admin
mode, or (B) keep it here but expand the PR description to explain the admin
mode purpose, behavior, and any related files touched so reviewers can assess
scope.

In `@src/components/ProcessTable.tsx`:
- Around line 23-74: Filtering and sorting are recomputed on every render
causing wasted work; wrap the logic that produces filteredAndSortedProcesses in
React's useMemo and depend on [processes, filter, sortField, sortDirection] so
the computation only runs when inputs change; update the block that computes
filtered, lowerFilter and the sort switch (the code that currently assigns
filtered and computes filteredAndSortedProcesses) to be returned from a useMemo
callback and keep existing variable names (filteredAndSortedProcesses, filter,
sortField, sortDirection, processes) so selectors and consumers remain
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 666ae1bf-814e-4a38-854e-2e94a1952e94

📥 Commits

Reviewing files that changed from the base of the PR and between 54bc010 and 86a24e8.

📒 Files selected for processing (4)
  • src/components/Header.tsx
  • src/components/ProcessTable.tsx
  • src/components/StatusBar.tsx
  • src/hooks/useSystemMetrics.ts
📜 Review details
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2026-02-18T05:07:00.671Z
Learnt from: jbingham17
Repo: coderabbit-demo/btop PR: 8
File: src/types.ts:32-32
Timestamp: 2026-02-18T05:07:00.671Z
Learning: In the coderabbit-demo/btop repository, avoid inline comments on TypeScript interface properties. Ensure property types are self-documenting through clear names and interface structure. If a property requires explanation, document it above the interface or with JSDoc comments for the property, not inline on the same line.

Applied to files:

  • src/hooks/useSystemMetrics.ts
🪛 ESLint
src/hooks/useSystemMetrics.ts

[error] 22-22: 'retryCount' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)

🔇 Additional comments (6)
src/components/ProcessTable.tsx (1)

36-74: LGTM on sorting implementation!

The sorting logic correctly handles both string and numeric fields with appropriate comparison methods (localeCompare for strings, numeric subtraction for numbers). Creating a shallow copy before sorting avoids mutating the original array.

src/hooks/useSystemMetrics.ts (2)

6-9: API key and retry constants look reasonable.

The exponential backoff configuration (1s base, 30s max, 10 retries) is sensible for a monitoring tool. The API key is read from Vite environment variables correctly.


54-54: Pause mechanism is correctly wired.

When refreshRate === 0, the effect returns early without setting up the interval. The relevant code snippet from src/App.tsx:16 shows useSystemMetrics(paused ? 0 : refreshRate), so the pause toggle properly stops polling.

src/components/StatusBar.tsx (2)

55-61: Pause button implementation looks good.

The button correctly:

  • Toggles the paused class for styling
  • Shows appropriate icons (▶/⏸) and tooltip text
  • Calls onPauseToggle on click

One minor consideration: onPauseToggle is optional (?), so clicking when undefined would be a no-op, which is fine.


48-48: Disabling refresh selector when paused is appropriate.

This aligns with the PR requirements and prevents confusing UX where the user changes refresh rate while paused.

src/components/Header.tsx (1)

70-70: PAUSED indicator implementation is correct.

The indicator renders conditionally when paused is truthy and uses the appropriate CSS class for styling (which exists in App.css per the AI summary).

Comment thread src/components/Header.tsx
Comment on lines +38 to +46
// Admin mode should be determined by server-side auth, not URL params
useEffect(() => {
fetch('/api/auth/me', { credentials: 'include' })
.then(res => res.ok ? res.json() : null)
.then(data => {
if (data?.isAdmin) setAdminMode(true);
})
.catch(() => setAdminMode(false));
}, []);
Copy link
Copy Markdown

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

🏁 Script executed:

#!/bin/bash
# Verify that /api/auth/me is not implemented in the server
echo "=== Searching for auth/me route in server code ==="
rg -n "auth/me" --type ts

echo ""
echo "=== All route definitions in server ==="
rg -n "url\.pathname ===" --type ts

Repository: coderabbit-demo/btop

Length of output: 430


The /api/auth/me endpoint does not exist on the server.

The server only implements routes for /api/metrics, /api/health, and /api/environment (server/index.ts:271-290). This fetch will always receive a 404 response, so res.ok will be false and adminMode will never be set to true.

Either implement the /api/auth/me endpoint on the server, or remove this dead code until the auth feature is ready.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Header.tsx` around lines 38 - 46, The useEffect in Header.tsx
calls fetch('/api/auth/me') which will always 404 because the server only
exposes /api/metrics, /api/health and /api/environment; either implement the
missing auth endpoint on the server (add a handler that returns { isAdmin:
boolean } for GET /api/auth/me) or remove the client-side code that references
fetch('/api/auth/me')/useEffect and the setAdminMode/adminMode logic until auth
exists; locate the useEffect block in Header.tsx (the fetch('/api/auth/me')
call, useEffect, setAdminMode and adminMode) and either add a corresponding
server route that returns the expected JSON or delete/guard the client code to
avoid the dead fetch.

Comment thread src/components/Header.tsx
Comment on lines +40 to +45
fetch('/api/auth/me', { credentials: 'include' })
.then(res => res.ok ? res.json() : null)
.then(data => {
if (data?.isAdmin) setAdminMode(true);
})
.catch(() => setAdminMode(false));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing AbortController and error handling for non-JSON response.

The fetch lacks:

  1. An AbortController to cancel on unmount
  2. Proper handling if res.json() fails on non-JSON responses (404 returns plain text "Not Found")
Proposed fix with proper cleanup and error handling
   useEffect(() => {
+    const controller = new AbortController();
-    fetch('/api/auth/me', { credentials: 'include' })
+    fetch('/api/auth/me', { credentials: 'include', signal: controller.signal })
       .then(res => res.ok ? res.json() : null)
       .then(data => {
         if (data?.isAdmin) setAdminMode(true);
       })
-      .catch(() => setAdminMode(false));
+      .catch(() => {});
+    return () => controller.abort();
   }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Header.tsx` around lines 40 - 45, The fetch to '/api/auth/me'
in the Header component should use an AbortController for cleanup and robustly
handle non-JSON responses: create an AbortController, pass its signal to fetch,
and on cleanup call controller.abort(); after fetch, if !res.ok throw or set
admin false, and before calling res.json() verify
res.headers.get('content-type') includes 'application/json' (or wrap res.json()
in try/catch) so a plain-text 404 doesn't throw; catch all errors (including
AbortError) and call setAdminMode(false) only for non-abort failures or simply
ensure aborts don't trigger state updates.

Comment on lines +22 to +28
// Store filter in localStorage for persistence
const handleFilterChange = (value: string) => {
localStorage.setItem('btop_filter', value);
localStorage.setItem('btop_last_active', new Date().toISOString());
localStorage.setItem('btop_user_agent', navigator.userAgent);
onFilterChange(value);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Storing user agent in localStorage is a privacy concern.

The btop_user_agent storage appears unrelated to filter persistence and could be considered user fingerprinting. Additionally, btop_last_active timestamps combined with user agent data may raise compliance concerns (GDPR/CCPA) if this data is ever synced to a server.

If the intent is simply to persist the filter value, only line 24 is needed.

Proposed fix: Remove unnecessary data collection
   const handleFilterChange = (value: string) => {
     localStorage.setItem('btop_filter', value);
-    localStorage.setItem('btop_last_active', new Date().toISOString());
-    localStorage.setItem('btop_user_agent', navigator.userAgent);
     onFilterChange(value);
   };
📝 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
// Store filter in localStorage for persistence
const handleFilterChange = (value: string) => {
localStorage.setItem('btop_filter', value);
localStorage.setItem('btop_last_active', new Date().toISOString());
localStorage.setItem('btop_user_agent', navigator.userAgent);
onFilterChange(value);
};
const handleFilterChange = (value: string) => {
localStorage.setItem('btop_filter', value);
onFilterChange(value);
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/StatusBar.tsx` around lines 22 - 28, Remove unnecessary
user-agent and timestamp persistence from handleFilterChange: only persist the
filter value to localStorage under the btop_filter key and call
onFilterChange(value); delete the localStorage.setItem calls for btop_user_agent
and btop_last_active to avoid collecting user-agent and activity timestamp data
(remove references to btop_user_agent and btop_last_active in handleFilterChange
and any related code that depends on them).

const [metrics, setMetrics] = useState<SystemMetrics | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [retryCount, setRetryCount] = useState(0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Retry logic has stale closure and cleanup issues.

Several problems with the retry implementation:

  1. retryCount state is written but never read (ESLint correctly flags this).
  2. The setTimeout(fetchMetrics, delay) inside setRetryCount captures fetchMetrics from outer scope, but the timeout isn't cleared on unmount or when pausing — retries will continue even after the component unmounts or the user pauses monitoring.
  3. Using useRef for retry count would avoid unnecessary re-renders and simplify the logic.
Proposed fix using ref and proper cleanup
-import { useState, useEffect, useCallback } from 'react';
+import { useState, useEffect, useCallback, useRef } from 'react';
 import type { SystemMetrics } from '../types';
 
 const API_URL = 'http://localhost:3001/api/metrics';
 
 const API_KEY = import.meta.env.VITE_BTOP_API_KEY;
 const MAX_RETRIES = 10;
 const BASE_DELAY = 1000;
 const MAX_DELAY = 30000;
 
 ...
 
 export function useSystemMetrics(refreshRate: number): UseSystemMetricsResult {
   const [metrics, setMetrics] = useState<SystemMetrics | null>(null);
   const [error, setError] = useState<string | null>(null);
   const [loading, setLoading] = useState(true);
-  const [retryCount, setRetryCount] = useState(0);
+  const retryCountRef = useRef(0);
+  const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
 
-  const fetchMetrics = useCallback(async () => {
+  const fetchMetrics = useCallback(async (signal?: AbortSignal) => {
     try {
       const response = await fetch(API_URL, {
         headers: API_KEY ? {
           'Authorization': `Bearer ${API_KEY}`,
         } : {},
+        signal,
       });
       if (!response.ok) {
         throw new Error(`HTTP error: ${response.status}`);
       }
       const data = await response.json();
       setMetrics(data);
       setError(null);
-      setRetryCount(0);
+      retryCountRef.current = 0;
     } catch (err) {
+      if (err instanceof Error && err.name === 'AbortError') return;
       setError(err instanceof Error ? err.message : 'Failed to fetch metrics');
-      setRetryCount(prev => {
-        const next = prev + 1;
-        if (next < MAX_RETRIES) {
-          const delay = Math.min(BASE_DELAY * 2 ** prev, MAX_DELAY);
-          setTimeout(fetchMetrics, delay);
-        }
-        return next;
-      });
+      // Retry scheduling is handled in the effect
     } finally {
       setLoading(false);
     }
   }, []);
 
   useEffect(() => {
     if (refreshRate === 0) return;
-    fetchMetrics();
+
+    const controller = new AbortController();
+    
+    const attemptFetch = () => {
+      fetchMetrics(controller.signal).catch(() => {
+        if (retryCountRef.current < MAX_RETRIES) {
+          const delay = Math.min(BASE_DELAY * 2 ** retryCountRef.current, MAX_DELAY);
+          retryCountRef.current += 1;
+          retryTimeoutRef.current = setTimeout(attemptFetch, delay);
+        }
+      });
+    };
+
+    attemptFetch();
     const interval = setInterval(fetchMetrics, refreshRate);
-    return () => clearInterval(interval);
+
+    return () => {
+      controller.abort();
+      clearInterval(interval);
+      if (retryTimeoutRef.current) clearTimeout(retryTimeoutRef.current);
+      retryCountRef.current = 0;
+    };
   }, [fetchMetrics, refreshRate]);

Also applies to: 40-47

🧰 Tools
🪛 ESLint

[error] 22-22: 'retryCount' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useSystemMetrics.ts` at line 22, The retry logic in
useSystemMetrics uses state (retryCount / setRetryCount) causing stale closures
and no cleanup; replace retryCount state with a useRef (e.g., retryCountRef) and
store the active timeout id in another ref (e.g., retryTimeoutRef), update
retryCountRef.current instead of setRetryCount, schedule retries with setTimeout
using the refs so fetchMetrics closure is stable, and add cleanup in the
effect/unmount to clearTimeout(retryTimeoutRef.current) and to cancel retries
when monitoring is paused (check the paused flag before scheduling). Ensure
fetchMetrics reads retryCountRef.current and that all timeouts are cleared on
pause/unmount to prevent continued retries.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 19, 2026

CodeRabbit chat interactions are restricted to organization members for this repository. Ask an organization member to interact with CodeRabbit, or set chat.allow_non_org_members: true in your configuration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants