Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,179 @@ body {
font-size: 10px;
}

/* Process Detail Panel */
.process-detail-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
backdrop-filter: blur(4px);
}

.process-detail-panel {
background: linear-gradient(145deg, #141c28 0%, #0d1219 100%);
border: 1px solid var(--border-color);
border-radius: 16px;
width: 480px;
max-height: 80vh;
overflow-y: auto;
Comment on lines +563 to +565
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

Panel width can overflow on narrow screens.

Using a fixed width: 480px risks clipping on smaller viewports. Prefer a responsive width cap.

Suggested fix
 .process-detail-panel {
   background: linear-gradient(145deg, `#141c28` 0%, `#0d1219` 100%);
   border: 1px solid var(--border-color);
   border-radius: 16px;
-  width: 480px;
+  width: min(480px, calc(100vw - 24px));
   max-height: 80vh;
   overflow-y: auto;
   box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6), 0 0 30px rgba(34, 211, 238, 0.1);
 }
📝 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
width: 480px;
max-height: 80vh;
overflow-y: auto;
width: min(480px, calc(100vw - 24px));
max-height: 80vh;
overflow-y: auto;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.css` around lines 563 - 565, The CSS rule uses a fixed width (width:
480px) which can overflow on narrow viewports; change it to a responsive pattern
(e.g., replace the fixed width with a fluid width and a cap such as width: 100%
combined with max-width: 480px, or use clamp() to enforce a min/ideal/max) so
the panel shrinks on small screens while keeping the 480px cap; update the block
containing the width: 480px declaration in src/App.css accordingly.

box-shadow: 0 8px 40px rgba(0, 0, 0, 0.6), 0 0 30px rgba(34, 211, 238, 0.1);
}

.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
}

.detail-header h3 {
color: var(--color-cyan);
font-size: 14px;
letter-spacing: 1px;
text-shadow: var(--glow-cyan);
}

.detail-close {
background: transparent;
border: 1px solid var(--border-color);
border-radius: 6px;
color: var(--color-text-dim);
width: 28px;
height: 28px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}

.detail-close:hover {
color: var(--color-red);
border-color: var(--color-red);
}

.detail-body {
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 16px;
}

.detail-section {
display: flex;
flex-direction: column;
gap: 8px;
}

.detail-row {
display: flex;
justify-content: space-between;
padding: 4px 0;
border-bottom: 1px solid rgba(42, 58, 80, 0.3);
}

.detail-label {
color: var(--color-text-dim);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
}

.detail-value {
color: var(--color-text);
font-weight: 600;
}

.detail-command {
background: rgba(0, 0, 0, 0.4);
padding: 10px;
border-radius: 8px;
color: var(--color-green);
font-size: 11px;
word-break: break-all;
line-height: 1.5;
}

.detail-notes {
background: rgba(0, 0, 0, 0.4);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 10px;
color: var(--color-text);
font-family: var(--font-mono);
font-size: 12px;
resize: vertical;
outline: none;
transition: border-color 0.2s;
}

.detail-notes:focus {
border-color: var(--color-cyan);
}

.signal-controls {
display: flex;
gap: 8px;
}

.signal-input {
flex: 1;
background: rgba(0, 0, 0, 0.4);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 8px 12px;
color: var(--color-text);
font-family: var(--font-mono);
font-size: 12px;
outline: none;
}

.signal-input:focus {
border-color: var(--color-cyan);
}

.detail-btn {
padding: 8px 16px;
border-radius: 8px;
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
border: 1px solid;
}

.detail-btn.secondary {
background: rgba(167, 139, 250, 0.1);
border-color: var(--color-purple);
color: var(--color-purple);
}

.detail-btn.secondary:hover {
background: rgba(167, 139, 250, 0.2);
}

.detail-btn.danger {
background: rgba(248, 113, 113, 0.15);
border-color: var(--color-red);
color: var(--color-red);
}

.detail-btn.danger:hover {
background: rgba(248, 113, 113, 0.3);
}

.kill-result {
color: var(--color-yellow);
font-size: 11px;
padding: 6px 10px;
background: rgba(251, 191, 36, 0.1);
border-radius: 6px;
}

/* Scrollbar styling */
.table-body::-webkit-scrollbar {
width: 6px;
Expand Down
137 changes: 137 additions & 0 deletions src/components/ProcessDetailPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { useState } from 'react';
import type { ProcessInfo } from '../types';

interface ProcessDetailPanelProps {
process: ProcessInfo;
onClose: () => void;
}

const API_URL = 'http://localhost:3001/api';
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

Kill API call is environment-bound and signal value is not URL-safe.

Hardcoding http://localhost:3001 breaks non-local deployments, and interpolating raw signal into the query string can mis-handle valid values (e.g., +).

Suggested fix
-const API_URL = 'http://localhost:3001/api';
+const API_URL = '/api';
@@
-      const response = await fetch(`${API_URL}/process/${process.pid}/kill?signal=${signal}`, {
+      const signalValue = signal.trim();
+      if (!signalValue) {
+        setKillResult('Signal is required');
+        return;
+      }
+      const params = new URLSearchParams({ signal: signalValue });
+      const response = await fetch(`${API_URL}/process/${process.pid}/kill?${params.toString()}`, {
         method: 'POST',
       });
+      if (!response.ok) {
+        throw new Error(`HTTP ${response.status}`);
+      }

Also applies to: 18-23

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

In `@src/components/ProcessDetailPanel.tsx` at line 9, The code hardcodes API_URL
and injects raw signal text into the query string; change to read the base URL
from an environment-configured variable (use the app's env mechanism, e.g.,
process.env.REACT_APP_API_URL or import.meta.env.VITE_API_URL) instead of the
literal API_URL constant, and ensure the kill request builds the URL safely by
using URL/URLSearchParams or encodeURIComponent for the signal parameter (e.g.,
construct `${baseUrl}/api/kill?signal=${encodeURIComponent(signal)}` or use new
URL(...) and url.searchParams.set('signal', signal)); update the place that
calls the kill endpoint (references to API_URL and the kill fetch/axios call in
ProcessDetailPanel.tsx) to use the env variable and URL-encoding so deployments
and special characters work correctly.


export function ProcessDetailPanel({ process, onClose }: ProcessDetailPanelProps) {
const [signal, setSignal] = useState('SIGTERM');
const [killResult, setKillResult] = useState<string | null>(null);
const [notes, setNotes] = useState('');

Comment on lines +14 to +15
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

Saved notes are never loaded back, so “retrieve notes” is incomplete.

The component writes to localStorage but does not hydrate notes when opening a process. Also, JSON.parse is unguarded and can crash the panel on corrupted storage.

Suggested fix
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
 import type { ProcessInfo } from '../types';
@@
 const API_URL = 'http://localhost:3001/api';
+const NOTES_KEY = 'process_notes';
@@
   const [signal, setSignal] = useState('SIGTERM');
   const [killResult, setKillResult] = useState<string | null>(null);
   const [notes, setNotes] = useState('');
+
+  const readNotes = (): Record<string, { notes?: string; command?: string; savedAt?: string }> => {
+    try {
+      return JSON.parse(localStorage.getItem(NOTES_KEY) || '{}');
+    } catch {
+      return {};
+    }
+  };
+
+  useEffect(() => {
+    const allNotes = readNotes();
+    setNotes(allNotes[String(process.pid)]?.notes || '');
+  }, [process.pid]);
@@
   const handleSaveNotes = () => {
-    const allNotes = JSON.parse(localStorage.getItem('process_notes') || '{}');
+    const allNotes = readNotes();
     allNotes[process.pid] = {
       notes,
       command: process.command,
       savedAt: new Date().toISOString(),
     };
-    localStorage.setItem('process_notes', JSON.stringify(allNotes));
+    localStorage.setItem(NOTES_KEY, JSON.stringify(allNotes));
   };

Also applies to: 28-36

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

In `@src/components/ProcessDetailPanel.tsx` around lines 14 - 15, The component
currently writes notes to localStorage but never hydrates the notes state;
update ProcessDetailPanel to load saved notes into the notes useState whenever a
process is opened (e.g., in an effect that depends on the active process id or
the panel open flag) by reading the same storage key you use when saving,
parsing it with JSON.parse inside a try/catch to guard against corrupted data,
and falling back to '' (or an empty object) on error; ensure you call
setNotes(...) with the loaded value and clear or reset corrupted storage if
parsing fails. Reference: notes, setNotes, and the component mount/open handler
or useEffect in ProcessDetailPanel.

const handleKillProcess = async () => {
try {
const response = await fetch(`${API_URL}/process/${process.pid}/kill?signal=${signal}`, {
method: 'POST',
});
const data = await response.json();
setKillResult(data.message);
} catch {
setKillResult('Failed to send signal');
}
};

const handleSaveNotes = () => {
const allNotes = JSON.parse(localStorage.getItem('process_notes') || '{}');
allNotes[process.pid] = {
notes,
command: process.command,
savedAt: new Date().toISOString(),
};
localStorage.setItem('process_notes', JSON.stringify(allNotes));
};

const formatMemory = (value: string): string => {
const num = parseInt(value);
if (num > 1048576) return `${(num / 1048576).toFixed(1)} GB`;
if (num > 1024) return `${(num / 1024).toFixed(1)} MB`;
return `${num} KB`;
};

return (
<div className="process-detail-overlay" onClick={onClose}>
<div className="process-detail-panel" onClick={(e) => e.stopPropagation()}>
<div className="detail-header">
<h3>Process Details</h3>
<button className="detail-close" onClick={onClose}>✕</button>
</div>

<div className="detail-body">
<div className="detail-section">
<div className="detail-row">
<span className="detail-label">PID</span>
<span className="detail-value">{process.pid}</span>
</div>
<div className="detail-row">
<span className="detail-label">User</span>
<span className="detail-value">{process.user}</span>
</div>
<div className="detail-row">
<span className="detail-label">CPU</span>
<span className="detail-value">{process.cpu.toFixed(1)}%</span>
</div>
<div className="detail-row">
<span className="detail-label">Memory</span>
<span className="detail-value">{process.mem.toFixed(1)}%</span>
</div>
<div className="detail-row">
<span className="detail-label">Virtual Memory</span>
<span className="detail-value">{formatMemory(process.vsz)}</span>
</div>
<div className="detail-row">
<span className="detail-label">Resident Memory</span>
<span className="detail-value">{formatMemory(process.rss)}</span>
</div>
<div className="detail-row">
<span className="detail-label">State</span>
<span className="detail-value">{process.stat}</span>
</div>
<div className="detail-row">
<span className="detail-label">Started</span>
<span className="detail-value">{process.start}</span>
</div>
<div className="detail-row">
<span className="detail-label">CPU Time</span>
<span className="detail-value">{process.time}</span>
</div>
</div>

<div className="detail-section">
<span className="detail-label">Command</span>
<div
className="detail-command"
dangerouslySetInnerHTML={{ __html: process.command }}
/>
Comment on lines +95 to +98
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:

fd -t f "ProcessDetailPanel.tsx"

Repository: coderabbit-demo/btop

Length of output: 101


🏁 Script executed:

wc -l src/components/ProcessDetailPanel.tsx

Repository: coderabbit-demo/btop

Length of output: 105


🏁 Script executed:

cat -n src/components/ProcessDetailPanel.tsx | sed -n '85,105p'

Repository: coderabbit-demo/btop

Length of output: 951


🏁 Script executed:

cat -n src/components/ProcessDetailPanel.tsx | head -50

Repository: coderabbit-demo/btop

Length of output: 2059


🏁 Script executed:

cat -n src/types.tsx

Repository: coderabbit-demo/btop

Length of output: 109


🏁 Script executed:

fd -t f "types\." src/

Repository: coderabbit-demo/btop

Length of output: 76


🏁 Script executed:

cat -n src/types.ts

Repository: coderabbit-demo/btop

Length of output: 1139


🏁 Script executed:

rg "ProcessInfo\|process.command" src/ -A 2 -B 2

Repository: coderabbit-demo/btop

Length of output: 46


🏁 Script executed:

rg "process\.command" src/ -B 3 -A 3

Repository: coderabbit-demo/btop

Length of output: 1615


🏁 Script executed:

rg "processes" src/ -B 2 -A 2 | grep -E "(fetch|api|endpoint|ProcessInfo)" -B 2 -A 2

Repository: coderabbit-demo/btop

Length of output: 484


🏁 Script executed:

rg "fetch.*api" src/ -B 2 -A 5

Repository: coderabbit-demo/btop

Length of output: 676


🏁 Script executed:

rg "API_URL\|/api" src/ -B 1 -A 3

Repository: coderabbit-demo/btop

Length of output: 46


🏁 Script executed:

rg "fetch.*process\|SystemMetrics" src/ -B 2 -A 5

Repository: coderabbit-demo/btop

Length of output: 46


🏁 Script executed:

fd -t f "App.tsx\|main.tsx\|index.tsx" src/

Repository: coderabbit-demo/btop

Length of output: 46


🏁 Script executed:

ls -la src/

Repository: coderabbit-demo/btop

Length of output: 645


🏁 Script executed:

cat -n src/App.tsx

Repository: coderabbit-demo/btop

Length of output: 3183


Remove dangerouslySetInnerHTML and render command as plain text.

The process.command field is rendered unsanitized as HTML through dangerouslySetInnerHTML, creating an XSS vulnerability. This is inconsistent with ProcessTable.tsx, which renders the same field as plain text. Command strings should always be rendered as text.

Suggested fix
           <div className="detail-section">
             <span className="detail-label">Command</span>
-            <div
-              className="detail-command"
-              dangerouslySetInnerHTML={{ __html: process.command }}
-            />
+            <div className="detail-command">{process.command}</div>
           </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
<div
className="detail-command"
dangerouslySetInnerHTML={{ __html: process.command }}
/>
<div className="detail-command">{process.command}</div>
🧰 Tools
🪛 ast-grep (0.41.1)

[warning] 96-96: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

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

In `@src/components/ProcessDetailPanel.tsx` around lines 95 - 98, Replace the
unsafe HTML rendering: in ProcessDetailPanel (the div with className
"detail-command" that currently uses dangerouslySetInnerHTML with
process.command) stop using dangerouslySetInnerHTML and render process.command
as plain text (e.g., inject the string as a child/JSX expression) so the command
is escaped and not parsed as HTML; match the approach used in ProcessTable.tsx
to eliminate the XSS risk.

</div>

<div className="detail-section">
<span className="detail-label">Notes</span>
<textarea
className="detail-notes"
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder="Add notes about this process..."
rows={3}
/>
<button className="detail-btn secondary" onClick={handleSaveNotes}>
Save Notes
</button>
</div>

<div className="detail-section signal-section">
<span className="detail-label">Send Signal</span>
<div className="signal-controls">
<input
type="text"
className="signal-input"
value={signal}
onChange={(e) => setSignal(e.target.value)}
placeholder="Signal name..."
/>
<button className="detail-btn danger" onClick={handleKillProcess}>
Send Signal
</button>
</div>
{killResult && (
<span className="kill-result">{killResult}</span>
)}
</div>
</div>
</div>
</div>
);
}
10 changes: 10 additions & 0 deletions src/components/ProcessTable.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useState, useMemo } from 'react';
import type { ProcessInfo, SortField, SortDirection } from '../types';
import { ProcessDetailPanel } from './ProcessDetailPanel';

interface ProcessTableProps {
processes: ProcessInfo[];
Expand All @@ -10,6 +11,7 @@ export function ProcessTable({ processes, filter }: ProcessTableProps) {
const [sortField, setSortField] = useState<SortField>('cpu');
const [sortDirection, setSortDirection] = useState<SortDirection>('desc');
const [selectedPid, setSelectedPid] = useState<number | null>(null);
const [detailProcess, setDetailProcess] = useState<ProcessInfo | null>(null);

const handleSort = (field: SortField) => {
if (sortField === field) {
Expand Down Expand Up @@ -139,6 +141,7 @@ export function ProcessTable({ processes, filter }: ProcessTableProps) {
key={process.pid}
className={`table-row ${selectedPid === process.pid ? 'selected' : ''}`}
onClick={() => setSelectedPid(process.pid === selectedPid ? null : process.pid)}
onDoubleClick={() => setDetailProcess(process)}
>
<span className="col-pid">{process.pid}</span>
<span className="col-user">{process.user.substring(0, 8)}</span>
Expand All @@ -156,6 +159,13 @@ export function ProcessTable({ processes, filter }: ProcessTableProps) {
</div>
))}
</div>

{detailProcess && (
<ProcessDetailPanel
process={detailProcess}
onClose={() => setDetailProcess(null)}
/>
)}
</div>
);
}
Loading