Skip to content

Commit ad05ba7

Browse files
committed
feat: resource monitoring
1 parent 71adbf4 commit ad05ba7

10 files changed

Lines changed: 652 additions & 207 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ A terminal UI application for managing your local development environment - hot
1414
- **Log Search** - Search and filter logs with regex support and match highlighting
1515
- **Log Export** - Export logs to files for debugging or sharing
1616
- **Service Details** - View detailed service info including PID, env vars, and uptime
17+
- **Resource Monitoring** - Real-time CPU and memory usage per service with sparkline graphs
1718
- **Hot Config Reload** - Update your config without restarting DevProc
1819
- **Keyboard-Driven** - Fast navigation with vim-style keybindings
1920

@@ -242,6 +243,7 @@ groups:
242243
| `Tab` | Toggle single/all logs view |
243244
| `c` | Clear logs |
244245
| `f` | Toggle follow mode |
246+
| `m` | Toggle resource monitor view |
245247
| `g` / `G` | Scroll to top / bottom |
246248
| `PgUp` / `PgDn` | Page up / down |
247249
| `Ctrl+u/d` | Half page up / down |

bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/ROADMAP.md

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This document outlines planned features and improvements for DevProc, organized by priority and complexity.
44

5-
## Current State (v0.4.x)
5+
## Current State (v0.5.x)
66

77
- [x] Service management (start, stop, restart)
88
- [x] Dependency ordering with healthchecks
@@ -15,6 +15,7 @@ This document outlines planned features and improvements for DevProc, organized
1515
- [x] Service details panel
1616
- [x] Animated spinners for starting/stopping
1717
- [x] Restart count badges
18+
- [x] Resource monitoring (CPU/memory per service)
1819

1920
---
2021

@@ -51,19 +52,19 @@ This document outlines planned features and improvements for DevProc, organized
5152

5253
---
5354

54-
## Medium Term (v0.5.x)
55+
## Completed (v0.5.x)
5556

5657
### Resource Monitoring
5758

58-
**Priority: High | Complexity: High**
59+
- [x] Real-time CPU % and memory usage in service list
60+
- [x] Resource graph in log panel (toggle with `m`)
61+
- [x] History tracking with sparkline visualization
62+
- [x] Color-coded CPU (green/yellow/red based on usage)
63+
- [x] Implementation: Uses `ps` command on macOS/Linux
5964

60-
Display CPU and memory usage per service.
65+
---
6166

62-
- Real-time CPU % and memory usage in service list
63-
- Optional resource graph in log panel (toggle with `m`)
64-
- Alerts when thresholds exceeded
65-
- History tracking for trends
66-
- Implementation: Use `ps` or `/proc` on Linux, `ps` on macOS
67+
## Medium Term (v0.6.x)
6768

6869
### Notifications
6970

@@ -274,15 +275,21 @@ Want to help build these features? Here's how:
274275

275276
### Help Wanted
276277

277-
- Resource monitoring (platform-specific code)
278278
- Docker API integration
279279
- Kubernetes support
280280

281281
---
282282

283283
## Changelog
284284

285-
### v0.4.0 (Current)
285+
### v0.5.0 (Current)
286+
287+
- Added resource monitoring (CPU/memory per service)
288+
- Real-time CPU % and memory usage displayed in service list
289+
- Resource graph view with sparkline visualization (toggle with `m`)
290+
- Color-coded CPU usage indicators
291+
292+
### v0.4.0
286293

287294
- Added log search with regex support
288295
- Added search match highlighting

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333
"build": "bun run scripts/build.ts",
3434
"build:all": "bun run scripts/build.ts --all",
3535
"test": "bun test",
36-
"typecheck": "tsc --noEmit"
36+
"typecheck": "tsc --noEmit",
37+
"prettier:write": "prettier --write ."
3738
},
3839
"devDependencies": {
40+
"prettier": "3.6.2",
3941
"@types/bun": "latest"
4042
},
4143
"peerDependencies": {
@@ -46,5 +48,9 @@
4648
"@opentui/solid": "^0.1.50",
4749
"yaml": "^2.8.1",
4850
"zod": "^4.1.13"
51+
},
52+
"prettier": {
53+
"semi": false,
54+
"printWidth": 120
4955
}
5056
}

src/app.tsx

Lines changed: 99 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { ScrollBoxRenderable } from "@opentui/core";
55
import type { ProcessManager } from "./process/manager";
66
import { useServices, type DisplayItem } from "./ui/hooks/useServices";
77
import { useLogs, type SearchMatch } from "./ui/hooks/useLogs";
8+
import { formatBytes, formatCpu, generateSparkline } from "./process/resources";
89

910
interface AppProps {
1011
manager: ProcessManager;
@@ -100,6 +101,9 @@ export function App(props: AppProps) {
100101
// Service details panel
101102
const [showDetails, setShowDetails] = createSignal(false);
102103

104+
// Resource graph view
105+
const [showResourceGraph, setShowResourceGraph] = createSignal(false);
106+
103107
// Spinner animation state
104108
const [spinnerFrame, setSpinnerFrame] = createSignal(0);
105109

@@ -569,6 +573,12 @@ export function App(props: AppProps) {
569573
setShowDetails(true);
570574
event.preventDefault();
571575
break;
576+
577+
// Resource graph toggle
578+
case "m":
579+
setShowResourceGraph((prev) => !prev);
580+
event.preventDefault();
581+
break;
572582
}
573583
},
574584
);
@@ -633,6 +643,9 @@ export function App(props: AppProps) {
633643
const indent = serviceItem.group ? " " : "";
634644
const restartBadge =
635645
service.restartCount > 0 ? `↻${service.restartCount}` : "";
646+
const resources = service.resources;
647+
const isRunning =
648+
service.status === "running" || service.status === "healthy";
636649
return (
637650
<box height={1} paddingLeft={1} paddingRight={1} flexDirection="row">
638651
<text>{indent}</text>
@@ -650,6 +663,14 @@ export function App(props: AppProps) {
650663
<Show when={restartBadge}>
651664
<text fg="yellow">{restartBadge} </text>
652665
</Show>
666+
<Show when={isRunning && resources}>
667+
<text fg={resources!.cpu > 80 ? "red" : resources!.cpu > 50 ? "yellow" : "cyan"}>
668+
{formatCpu(resources!.cpu).padStart(5)}
669+
</text>
670+
<text> </text>
671+
<text fg="cyan">{formatBytes(resources!.memory).padStart(7)}</text>
672+
<text> </text>
673+
</Show>
653674
<text fg="gray">{service.port ? `:${service.port}` : ""}</text>
654675
<text> </text>
655676
<text fg="gray">{formatUptime(service.startedAt)}</text>
@@ -666,28 +687,92 @@ export function App(props: AppProps) {
666687
{/* Log panel */}
667688
<box flexGrow={1} flexDirection="column" borderStyle="rounded" borderColor="gray">
668689
<box height={1} paddingLeft={1} flexDirection="row">
669-
<text fg="cyan" attributes={TextAttributes.BOLD}>
670-
Logs{" "}
671-
{viewMode() === "single" && selectedService()
672-
? `(${selectedService()!.name})`
673-
: "(all)"}
674-
</text>
690+
<Show
691+
when={showResourceGraph()}
692+
fallback={
693+
<text fg="cyan" attributes={TextAttributes.BOLD}>
694+
Logs{" "}
695+
{viewMode() === "single" && selectedService()
696+
? `(${selectedService()!.name})`
697+
: "(all)"}
698+
</text>
699+
}
700+
>
701+
<text fg="cyan" attributes={TextAttributes.BOLD}>
702+
Resources{" "}
703+
{selectedService() ? `(${selectedService()!.name})` : ""}
704+
</text>
705+
</Show>
675706
<box flexGrow={1} />
676-
<Show when={isSearchActive()}>
707+
<Show when={!showResourceGraph() && isSearchActive()}>
677708
<text fg="yellow">
678709
/{searchQuery()} [{currentMatchIndex() + 1}/{searchMatches().length}]{" "}
679710
</text>
680711
</Show>
681-
<Show when={searchMode()}>
712+
<Show when={!showResourceGraph() && searchMode()}>
682713
<text fg="cyan">/{searchInput()}_</text>
683714
</Show>
684-
<Show when={!searchMode() && !following() && scrollInfo().total > 0}>
715+
<Show when={!showResourceGraph() && !searchMode() && !following() && scrollInfo().total > 0}>
685716
<text fg="gray">
686717
{scrollInfo().current}/{scrollInfo().total}{" "}
687718
</text>
688719
</Show>
689-
<text fg={following() ? "green" : "gray"}>{following() ? "[follow]" : "[scroll]"}</text>
720+
<Show when={!showResourceGraph()}>
721+
<text fg={following() ? "green" : "gray"}>{following() ? "[follow]" : "[scroll]"}</text>
722+
</Show>
723+
<Show when={showResourceGraph()}>
724+
<text fg="cyan">[m] logs</text>
725+
</Show>
690726
</box>
727+
728+
{/* Resource graph view */}
729+
<Show when={showResourceGraph() && selectedService()}>
730+
{(() => {
731+
const service = selectedService()!;
732+
const history = props.manager.getResourceHistory(service.name);
733+
const cpuValues = history.map((h) => h.cpu);
734+
const memValues = history.map((h) => h.memory / (1024 * 1024)); // Convert to MB for display
735+
const resources = service.resources;
736+
737+
return (
738+
<box flexDirection="column" padding={1} flexGrow={1}>
739+
<text fg="yellow" attributes={TextAttributes.BOLD}>
740+
CPU Usage
741+
</text>
742+
<box height={1} flexDirection="row">
743+
<text fg="cyan">{generateSparkline(cpuValues, 50)}</text>
744+
<text> </text>
745+
<text fg={resources && resources.cpu > 80 ? "red" : resources && resources.cpu > 50 ? "yellow" : "green"}>
746+
{resources ? formatCpu(resources.cpu) : "N/A"}
747+
</text>
748+
</box>
749+
<text> </text>
750+
<text fg="yellow" attributes={TextAttributes.BOLD}>
751+
Memory Usage
752+
</text>
753+
<box height={1} flexDirection="row">
754+
<text fg="magenta">{generateSparkline(memValues, 50)}</text>
755+
<text> </text>
756+
<text fg="green">{resources ? formatBytes(resources.memory) : "N/A"}</text>
757+
</box>
758+
<text> </text>
759+
<text fg="gray">
760+
History: {history.length} samples ({history.length}s)
761+
</text>
762+
<Show when={resources}>
763+
<text fg="gray">
764+
Memory %: {resources!.memoryPercent.toFixed(1)}% of system
765+
</text>
766+
</Show>
767+
<box flexGrow={1} />
768+
<text fg="gray">Press [m] to return to logs</text>
769+
</box>
770+
);
771+
})()}
772+
</Show>
773+
774+
{/* Logs view */}
775+
<Show when={!showResourceGraph()}>
691776
<scrollbox
692777
ref={(el: ScrollBoxRenderable) => (scrollboxRef = el)}
693778
flexGrow={1}
@@ -785,6 +870,7 @@ export function App(props: AppProps) {
785870
</For>
786871
</box>
787872
</scrollbox>
873+
</Show>
788874
</box>
789875
</box>
790876

@@ -803,6 +889,8 @@ export function App(props: AppProps) {
803889
<text>lear </text>
804890
<text fg="gray">[f]</text>
805891
<text>ollow </text>
892+
<text fg="gray">[m]</text>
893+
<text>onitor </text>
806894
<text fg="gray">| </text>
807895
<text fg="gray">[?]</text>
808896
<text>help </text>
@@ -834,7 +922,7 @@ export function App(props: AppProps) {
834922
<text>Space Toggle group | i Service info</text>
835923
<text> </text>
836924
<text fg="yellow">Logs</text>
837-
<text>Tab Toggle view | c Clear | f Follow</text>
925+
<text>Tab Toggle view | c Clear | f Follow | m Monitor</text>
838926
<text>g/G Top/bottom | PgUp/PgDn | Ctrl+u/d</text>
839927
<text>e Export service logs | E Export all logs</text>
840928
<text> </text>

src/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { loadConfig } from "./config/loader";
55
import { ProcessManager } from "./process/manager";
66
import { App } from "./app";
77

8-
const VERSION = "0.4.0";
8+
const VERSION = "0.5.0";
99

1010
function printHelp() {
1111
console.log(`

0 commit comments

Comments
 (0)