salvetron 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +333 -0
  2. package/bin/salvetron.tsx +9 -0
  3. package/package.json +43 -0
  4. package/scripts/salvetron-sim.mjs +75 -0
  5. package/src/app.tsx +62 -0
  6. package/src/modules/dashboard/store/dashboard.store.ts +19 -0
  7. package/src/modules/dashboard/ui/components/metric-row/index.ts +1 -0
  8. package/src/modules/dashboard/ui/components/metric-row/metric-row.tsx +25 -0
  9. package/src/modules/dashboard/ui/components/performance-panel/index.ts +1 -0
  10. package/src/modules/dashboard/ui/components/performance-panel/performance-panel.tsx +67 -0
  11. package/src/modules/dashboard/ui/containers/dashboard-container/dashboard-container.tsx +290 -0
  12. package/src/modules/dashboard/ui/containers/dashboard-container/index.ts +1 -0
  13. package/src/modules/js-logs/store/js-logs.store.ts +21 -0
  14. package/src/modules/js-logs/ui/components/log-detail/index.ts +1 -0
  15. package/src/modules/js-logs/ui/components/log-detail/log-detail.tsx +62 -0
  16. package/src/modules/js-logs/ui/components/log-list/index.ts +1 -0
  17. package/src/modules/js-logs/ui/components/log-list/log-list.tsx +32 -0
  18. package/src/modules/js-logs/ui/containers/js-logs-container/index.ts +1 -0
  19. package/src/modules/js-logs/ui/containers/js-logs-container/js-logs-container.tsx +80 -0
  20. package/src/modules/native-logs/store/native-logs.store.ts +21 -0
  21. package/src/modules/native-logs/ui/components/native-log-detail/index.ts +1 -0
  22. package/src/modules/native-logs/ui/components/native-log-detail/native-log-detail.tsx +63 -0
  23. package/src/modules/native-logs/ui/components/native-log-list/index.ts +1 -0
  24. package/src/modules/native-logs/ui/components/native-log-list/native-log-list.tsx +33 -0
  25. package/src/modules/native-logs/ui/containers/native-logs-container/index.ts +1 -0
  26. package/src/modules/native-logs/ui/containers/native-logs-container/native-logs-container.tsx +80 -0
  27. package/src/modules/network/library/constants.ts +15 -0
  28. package/src/modules/network/store/network.store.ts +58 -0
  29. package/src/modules/network/ui/components/network-detail/index.ts +1 -0
  30. package/src/modules/network/ui/components/network-detail/network-detail.tsx +82 -0
  31. package/src/modules/network/ui/components/network-row/index.ts +1 -0
  32. package/src/modules/network/ui/components/network-row/network-row.tsx +23 -0
  33. package/src/modules/network/ui/components/network-table-header/index.ts +1 -0
  34. package/src/modules/network/ui/components/network-table-header/network-table-header.tsx +12 -0
  35. package/src/modules/network/ui/containers/network-container/index.ts +1 -0
  36. package/src/modules/network/ui/containers/network-container/network-container.tsx +93 -0
  37. package/src/server/ws-server.ts +52 -0
  38. package/src/shared/components/ascii-logo/ascii-logo.tsx +169 -0
  39. package/src/shared/components/ascii-logo/ascii.txt +0 -0
  40. package/src/shared/components/ascii-logo/index.ts +1 -0
  41. package/src/shared/components/gauge/gauge.tsx +18 -0
  42. package/src/shared/components/gauge/index.ts +1 -0
  43. package/src/shared/components/log-row/index.ts +1 -0
  44. package/src/shared/components/log-row/log-row.tsx +28 -0
  45. package/src/shared/components/panel/index.ts +1 -0
  46. package/src/shared/components/panel/panel.tsx +28 -0
  47. package/src/shared/components/sparkline/index.ts +1 -0
  48. package/src/shared/components/sparkline/sparkline.tsx +26 -0
  49. package/src/shared/components/status-bar/index.ts +1 -0
  50. package/src/shared/components/status-bar/status-bar.tsx +32 -0
  51. package/src/shared/components/tab-bar/index.ts +1 -0
  52. package/src/shared/components/tab-bar/tab-bar.tsx +36 -0
  53. package/src/shared/hooks/use-detail-panel.ts +78 -0
  54. package/src/shared/hooks/use-list-navigation.ts +44 -0
  55. package/src/shared/hooks/use-terminal-size.ts +42 -0
  56. package/src/shared/store/device.store.ts +25 -0
  57. package/src/shared/types.ts +3 -0
  58. package/src/shared/utils/build-curl-command.ts +19 -0
  59. package/src/shared/utils/clipboard.ts +27 -0
  60. package/src/shared/utils/format-body.ts +21 -0
package/README.md ADDED
@@ -0,0 +1,333 @@
1
+ <p align="center">
2
+ <img src="assets/banner.png" alt="Salvetron Logo" width="1280" height="720" style="border-radius: 20px;">
3
+ </p>
4
+
5
+ <h1 align="center">Salvetron</h1>
6
+
7
+ <p align="center">
8
+ <strong>Real-time terminal UI debugger for React Native</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <img src="https://img.shields.io/badge/React%20Native-0.73+-61dafb?style=flat-square&logo=react" alt="React Native">
13
+ <img src="https://img.shields.io/badge/runtime-Node%2018+-339933?style=flat-square&logo=node.js" alt="Node">
14
+ <img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License">
15
+ </p>
16
+
17
+ ---
18
+
19
+ Salvetron is a real-time debugging tool for React Native developers, delivered as a terminal UI (TUI). The Salvetron CLI runs a WebSocket server in your terminal and renders incoming telemetry — JS logs, native logs, network traffic, and live performance metrics — from your running app. Your app streams that telemetry using the companion SDK, `@salve-software/salvetron-react-native`.
20
+
21
+ ## Features
22
+
23
+ - **Dashboard** - Live performance overview: UI/JS FPS, memory and CPU usage, with sparkline history
24
+ - **JS Logs** - Stream JavaScript console logs with level filtering (debug, info, warn, error) and an expandable detail panel for metadata
25
+ - **Network Inspector** - Inspect HTTP requests and responses: method, status, duration, headers, and pretty-printed bodies
26
+ - **Native Logs** - View iOS and Android platform logs in real-time, with source tags
27
+ - **Keyboard-driven TUI** - Navigate panels and lists entirely from the keyboard; no GUI required
28
+ - **Zero-config WebSocket server** - Starts on port 8765 by default (override with `SALVETRON_PORT`)
29
+
30
+ ## Screenshots
31
+
32
+ <!-- Add your screenshots here -->
33
+
34
+ <p align="center">
35
+ <em>Screenshots coming soon...</em>
36
+ </p>
37
+
38
+ <!--
39
+ Example:
40
+ <p align="center">
41
+ <img src="assets/screenshots/main-view.png" alt="Main View" width="800">
42
+ </p>
43
+ -->
44
+
45
+ ## Architecture
46
+
47
+ Salvetron has two parts:
48
+
49
+ ```
50
+ ┌──────────────────────────┐ WebSocket ┌──────────────────────────┐
51
+ │ React Native App │ ───────────────────────▶ │ Salvetron CLI │
52
+ │ @salve-software/ │ port 8765 │ (terminal UI debugger) │
53
+ │ salvetron-react-native │ │ │
54
+ └──────────────────────────┘ └──────────────────────────┘
55
+ │ │
56
+ ├── JS Console Logs ├── Dashboard (FPS/CPU/memory)
57
+ ├── Native Logs (iOS/Android) ├── JS Logs viewer
58
+ ├── Network Requests/Responses ├── Network Inspector
59
+ └── Performance Metrics └── Native Logs viewer
60
+ ```
61
+
62
+ 1. **Salvetron CLI** (`salvetron`): an Ink-based terminal UI that runs a WebSocket server and renders telemetry from connected apps.
63
+
64
+ 2. **salvetron-react-native** (SDK): A React Native package built with Nitro Modules that captures logs, network activity, and performance metrics and streams them to the CLI.
65
+
66
+ ## Installation
67
+
68
+ ### Requirements
69
+
70
+ | Component | Requirement |
71
+ |-----------|-------------|
72
+ | Salvetron CLI | Node.js 18+ |
73
+ | React Native SDK | React Native 0.73+ |
74
+ | Xcode | 15+ (for iOS development) |
75
+ | Android Studio | Latest (for Android development) |
76
+
77
+ ### Running the Salvetron CLI
78
+
79
+ 1. Clone the repository:
80
+ ```bash
81
+ git clone https://github.com/Salve-Software/salvetron.git
82
+ cd salvetron
83
+ yarn install
84
+ ```
85
+
86
+ 2. Start the CLI:
87
+ ```bash
88
+ yarn dev # starts the Salvetron CLI (Ink TUI) on port 8765
89
+ ```
90
+
91
+ Or, once published:
92
+ ```bash
93
+ npx salvetron
94
+ ```
95
+
96
+ Override the port with the `SALVETRON_PORT` environment variable:
97
+ ```bash
98
+ SALVETRON_PORT=9000 yarn dev
99
+ ```
100
+
101
+ ### Installing React Native SDK
102
+
103
+ 1. Install the package in your React Native project:
104
+
105
+ ```bash
106
+ # Using npm
107
+ npm install @salve-software/salvetron-react-native
108
+
109
+ # Using yarn
110
+ yarn add @salve-software/salvetron-react-native
111
+ ```
112
+
113
+ 2. For iOS, install pods:
114
+ ```bash
115
+ cd ios && pod install && cd ..
116
+ ```
117
+
118
+ 3. For Android, the package will auto-link. Run a gradle sync if needed.
119
+
120
+ ## Usage
121
+
122
+ ### Starting the Salvetron CLI
123
+
124
+ Run `yarn dev` (or `npx salvetron`) in your terminal. The CLI starts a WebSocket server on port **8765** and shows the Dashboard. Use the tab bar / keyboard shortcuts shown in the status bar to switch between Dashboard, JS Logs, Network, and Native Logs. Connected device info appears in the header once your app connects.
125
+
126
+ ### Connecting from React Native
127
+
128
+ Add the following code to your React Native app's entry point (e.g., `App.tsx` or `index.js`):
129
+
130
+ ```typescript
131
+ import Salvetron from '@salve-software/salvetron-react-native';
132
+
133
+ // Connect only in development mode
134
+ if (__DEV__) {
135
+ Salvetron.connect({
136
+ host: '192.168.1.100', // Your Mac's IP address
137
+ port: 8765,
138
+ enableNetworkCapture: true,
139
+ onConnect: () => console.log('Connected to Salvetron!'),
140
+ onDisconnect: () => console.log('Disconnected from Salvetron'),
141
+ onError: (error) => console.error('Salvetron error:', error),
142
+ });
143
+ }
144
+ ```
145
+
146
+ > **Tip**: Use `localhost` when running on iOS Simulator, or your Mac's local IP address for physical devices.
147
+
148
+ ### API Reference
149
+
150
+ #### `Salvetron.connect(config?)`
151
+
152
+ Establishes a WebSocket connection to the Salvetron CLI.
153
+
154
+ ```typescript
155
+ interface SalvetronConfig {
156
+ host?: string; // Default: 'localhost'
157
+ port?: number; // Default: 8765
158
+ enableNetworkCapture?: boolean; // Default: true
159
+ ignoredUrls?: RegExp[]; // URL patterns to ignore
160
+ onConnect?: () => void;
161
+ onDisconnect?: () => void;
162
+ onError?: (error: Error) => void;
163
+ }
164
+ ```
165
+
166
+ #### `Salvetron.disconnect()`
167
+
168
+ Closes the WebSocket connection.
169
+
170
+ ```typescript
171
+ Salvetron.disconnect();
172
+ ```
173
+
174
+ #### `Salvetron.isConnected()`
175
+
176
+ Returns the current connection status.
177
+
178
+ ```typescript
179
+ const connected = Salvetron.isConnected(); // boolean
180
+ ```
181
+
182
+ #### Logging Methods
183
+
184
+ ```typescript
185
+ // Send logs with different levels
186
+ Salvetron.log('General log message');
187
+ Salvetron.debug('Debug information', { userId: 123 });
188
+ Salvetron.info('Informational message');
189
+ Salvetron.warn('Warning message');
190
+ Salvetron.error('Error message', { stack: error.stack });
191
+ ```
192
+
193
+ All logging methods accept an optional metadata object as the second parameter.
194
+
195
+ ## Troubleshooting
196
+
197
+ ### Connection Issues
198
+
199
+ **Problem**: App can't connect to the Salvetron CLI
200
+
201
+ **Solutions**:
202
+ 1. Ensure the Salvetron CLI is running (`yarn dev` / `npx salvetron`)
203
+ 2. Check that both devices are on the same network
204
+ 3. Verify the IP address is correct (use `ifconfig` to find your machine's IP)
205
+ 4. Check if port 8765 is not blocked by firewall
206
+ 5. For iOS Simulator, use `localhost` instead of IP address
207
+
208
+ ### Network Requests Not Showing
209
+
210
+ **Problem**: HTTP requests are not appearing in the Network tab
211
+
212
+ **Solutions**:
213
+ 1. Ensure `enableNetworkCapture: true` is set in the config
214
+ 2. Check if the URL isn't in the `ignoredUrls` list
215
+ 3. Default ignored URLs include Metro bundler (port 8081) and hot reload endpoints
216
+
217
+ ### Logs Not Appearing
218
+
219
+ **Problem**: Console logs are not showing in the Salvetron CLI
220
+
221
+ **Solutions**:
222
+ 1. Verify the connection is established (`Salvetron.isConnected()`)
223
+ 2. Ensure you're running in development mode (`__DEV__ === true`)
224
+ 3. Confirm the CLI shows your device in the header
225
+
226
+ ### High Memory Usage
227
+
228
+ **Problem**: The Salvetron CLI process using too much memory
229
+
230
+ **Solutions**:
231
+ 1. Clear logs periodically from the JS Logs / Native Logs panels
232
+ 2. Reduce the number of connected devices
233
+ 3. Filter out verbose logs at the source
234
+
235
+ ## Contributing
236
+
237
+ We welcome contributions! Here's how you can help:
238
+
239
+ ### Getting Started
240
+
241
+ 1. Fork the repository
242
+ 2. Clone your fork:
243
+ ```bash
244
+ git clone https://github.com/your-username/salvetron.git
245
+ ```
246
+ 3. Create a new branch:
247
+ ```bash
248
+ git checkout -b feature/your-feature-name
249
+ ```
250
+
251
+ ### Branch Naming Convention
252
+
253
+ - `feature/` - New features
254
+ - `fix/` - Bug fixes
255
+ - `docs/` - Documentation updates
256
+ - `refactor/` - Code refactoring
257
+ - `test/` - Adding or updating tests
258
+
259
+ ### Commit Message Format
260
+
261
+ Follow conventional commits:
262
+ ```
263
+ type(scope): description
264
+
265
+ Examples:
266
+ feat(sdk): add custom log levels support
267
+ fix(app): resolve memory leak in log viewer
268
+ docs(readme): update installation instructions
269
+ ```
270
+
271
+ ### Pull Request Process
272
+
273
+ 1. Ensure your code follows the existing style
274
+ 2. Update documentation if needed
275
+ 3. Test your changes thoroughly:
276
+ - For Salvetron CLI: run `yarn dev` and verify with the simulator (`yarn sim`)
277
+ - For SDK: Test with the example app
278
+ 4. Create a Pull Request with a clear description
279
+
280
+ ### Code Style Guidelines
281
+
282
+ **salvetron (CLI, TypeScript / Ink)**:
283
+ - Use TypeScript for all source files
284
+ - Keep modules organized under `src/modules/<feature>` and shared UI under `src/shared`
285
+ - Run `yarn typecheck` before opening a PR
286
+
287
+ **salvetron-react-native (SDK, TypeScript)**:
288
+ - Use TypeScript for all source files
289
+ - Follow existing patterns in the codebase
290
+ - Document public APIs with JSDoc comments
291
+
292
+ ### Running the Example App
293
+
294
+ ```bash
295
+ cd packages/salvetron-react-native/example
296
+ yarn install
297
+ cd ios && pod install && cd ..
298
+ yarn ios # or yarn android
299
+ ```
300
+
301
+ ## License
302
+
303
+ This project is licensed under the MIT License - see below for details:
304
+
305
+ ```
306
+ MIT License
307
+
308
+ Copyright (c) 2024 Salvetron Contributors
309
+
310
+ Permission is hereby granted, free of charge, to any person obtaining a copy
311
+ of this software and associated documentation files (the "Software"), to deal
312
+ in the Software without restriction, including without limitation the rights
313
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
314
+ copies of the Software, and to permit persons to whom the Software is
315
+ furnished to do so, subject to the following conditions:
316
+
317
+ The above copyright notice and this permission notice shall be included in all
318
+ copies or substantial portions of the Software.
319
+
320
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
321
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
322
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
323
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
324
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
325
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
326
+ SOFTWARE.
327
+ ```
328
+
329
+ ---
330
+
331
+ <p align="center">
332
+ Made by Salve Software
333
+ </p>
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env -S tsx
2
+ import { render } from 'ink'
3
+ import { App } from '../src/app.js'
4
+ import { startWsServer } from '../src/server/ws-server.js'
5
+
6
+ const PORT = Number(process.env.SALVETRON_PORT ?? 8765)
7
+
8
+ startWsServer(PORT)
9
+ render(<App />, { patchConsole: false })
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "salvetron",
3
+ "description": "Terminal UI debugger for React Native",
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "packageManager": "yarn@4.9.1",
7
+ "workspaces": [
8
+ "packages/salvetron-types"
9
+ ],
10
+ "bin": "./bin/salvetron.tsx",
11
+ "files": [
12
+ "bin",
13
+ "src",
14
+ "scripts"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "scripts": {
20
+ "dev": "tsx bin/salvetron.tsx",
21
+ "start": "tsx bin/salvetron.tsx",
22
+ "sim": "node scripts/salvetron-sim.mjs",
23
+ "typecheck": "yarn build && tsc --noEmit",
24
+ "build": "yarn workspace @salve-software/salvetron-types build",
25
+ "test": "yarn workspaces foreach -Apt run test"
26
+ },
27
+ "dependencies": {
28
+ "@salve-software/salvetron-types": "^1.0.0",
29
+ "figlet": "^1.11.0",
30
+ "ink": "^7.0.6",
31
+ "json-colorizer": "^3.0.1",
32
+ "react": "^19.1.0",
33
+ "ws": "^8.18.0",
34
+ "zustand": "^5.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/figlet": "^1.7.0",
38
+ "@types/react": "^19.0.0",
39
+ "@types/ws": "^8.5.0",
40
+ "tsx": "^4.0.0",
41
+ "typescript": "~5.8.3"
42
+ }
43
+ }
@@ -0,0 +1,75 @@
1
+ import WebSocket from 'ws'
2
+
3
+ const PORT = Number(process.env.SALVETRON_PORT ?? 8765)
4
+ const ws = new WebSocket(`ws://localhost:${PORT}`)
5
+
6
+ ws.on('open', () => {
7
+ const send = (e) => ws.send(JSON.stringify(e))
8
+
9
+ send({ type: 'device_info', deviceName: 'iPhone 15 Sim', platform: 'ios' })
10
+
11
+ for (let i = 0; i < 12; i++) {
12
+ send({
13
+ type: 'log',
14
+ timestamp: Date.now(),
15
+ level: ['info', 'warn', 'error', 'debug', 'log'][i % 5],
16
+ source: 'js',
17
+ message: `JS log message number ${i} - something happened in the app that produced a fairly long message to test truncation`,
18
+ metadata: { index: i, nested: { a: 1, b: 'test' } },
19
+ })
20
+ }
21
+
22
+ for (let i = 0; i < 8; i++) {
23
+ const requestId = `req-${i}`
24
+ send({
25
+ type: 'network',
26
+ stage: 'request',
27
+ timestamp: Date.now(),
28
+ requestId,
29
+ method: ['GET', 'POST', 'PUT', 'DELETE'][i % 4],
30
+ url: `https://api.example.com/v1/resource/${i}?foo=bar&long=query-param-to-test-truncation`,
31
+ headers: { 'Content-Type': 'application/json' },
32
+ body: i % 2 === 0 ? JSON.stringify({ hello: 'world' }) : undefined,
33
+ })
34
+ send({
35
+ type: 'network',
36
+ stage: 'response',
37
+ timestamp: Date.now(),
38
+ requestId,
39
+ method: ['GET', 'POST', 'PUT', 'DELETE'][i % 4],
40
+ url: `https://api.example.com/v1/resource/${i}`,
41
+ statusCode: [200, 201, 404, 500][i % 4],
42
+ duration: 50 + i * 10,
43
+ headers: { 'Content-Type': 'application/json' },
44
+ body: JSON.stringify({ result: 'ok', index: i, items: [1, 2, 3] }),
45
+ })
46
+ }
47
+
48
+ for (let i = 0; i < 10; i++) {
49
+ send({
50
+ type: 'native',
51
+ timestamp: Date.now(),
52
+ level: ['info', 'warn', 'error'][i % 3],
53
+ source: i % 2 === 0 ? 'ios' : 'android',
54
+ tag: 'AppLifecycle',
55
+ message: `Native event ${i} fired with some descriptive text to check wrapping behavior in the panel`,
56
+ metadata: { i },
57
+ })
58
+ }
59
+
60
+ let tick = 0
61
+ const perfInterval = setInterval(() => {
62
+ tick++
63
+ send({
64
+ type: 'performance_metrics',
65
+ timestamp: Date.now(),
66
+ uiFps: 55 + Math.round(Math.sin(tick) * 4),
67
+ jsFps: 50 + Math.round(Math.cos(tick) * 5),
68
+ memoryUsage: 180 + tick * 3,
69
+ cpuUsage: 20 + (tick % 30),
70
+ })
71
+ if (tick > 30) clearInterval(perfInterval)
72
+ }, 200)
73
+ })
74
+
75
+ ws.on('error', (e) => console.error('ws error', e))
package/src/app.tsx ADDED
@@ -0,0 +1,62 @@
1
+ import { Box, useInput } from 'ink'
2
+ import { useMemo, useState } from 'react'
3
+ import { AsciiLogo, pickRandomColor } from './shared/components/ascii-logo/index.js'
4
+ import { TabBar } from './shared/components/tab-bar/index.js'
5
+ import { StatusBar } from './shared/components/status-bar/index.js'
6
+ import { useProject } from './shared/store/device.store.js'
7
+ import { DashboardContainer } from './modules/dashboard/ui/containers/dashboard-container/index.js'
8
+ import { JsLogsContainer } from './modules/js-logs/ui/containers/js-logs-container/index.js'
9
+ import { NetworkContainer } from './modules/network/ui/containers/network-container/index.js'
10
+ import { NativeLogsContainer } from './modules/native-logs/ui/containers/native-logs-container/index.js'
11
+
12
+ export type Tab = 'dashboard' | 'js-logs' | 'network' | 'native'
13
+
14
+ const TABS: Tab[] = ['dashboard', 'js-logs', 'network', 'native']
15
+ const DEFAULT_LOGO_COLOR = '#61DAFB'
16
+
17
+ export function App() {
18
+ const [activeTab, setActiveTab] = useState<Tab>('dashboard')
19
+ const appName = useProject()?.appName
20
+
21
+ const logoColor = useMemo(
22
+ () => (appName ? pickRandomColor() : DEFAULT_LOGO_COLOR),
23
+ [appName],
24
+ )
25
+
26
+ useInput((input, key) => {
27
+ if (input === '1') setActiveTab('dashboard')
28
+ if (input === '2') setActiveTab('js-logs')
29
+ if (input === '3') setActiveTab('network')
30
+ if (input === '4') setActiveTab('native')
31
+ if (key.tab) {
32
+ const i = TABS.indexOf(activeTab)
33
+ setActiveTab(TABS[(i + 1) % TABS.length])
34
+ }
35
+ })
36
+
37
+ return (
38
+ <Box flexDirection="column" height="100%" paddingTop={1}>
39
+ <AsciiLogo text={appName} color={logoColor} />
40
+ <TabBar active={activeTab} />
41
+ <Box flexGrow={1} flexDirection="column" paddingX={1}>
42
+ {activeTab === 'dashboard'
43
+ ? <DashboardContainer />
44
+ : null
45
+ }
46
+ {activeTab === 'js-logs'
47
+ ? <JsLogsContainer />
48
+ : null
49
+ }
50
+ {activeTab === 'network'
51
+ ? <NetworkContainer />
52
+ : null
53
+ }
54
+ {activeTab === 'native'
55
+ ? <NativeLogsContainer />
56
+ : null
57
+ }
58
+ </Box>
59
+ <StatusBar />
60
+ </Box>
61
+ )
62
+ }
@@ -0,0 +1,19 @@
1
+ import { create } from 'zustand'
2
+ import type { PerformanceMetricsEvent } from '@salve-software/salvetron-types'
3
+
4
+ interface DashboardStore {
5
+ snapshots: PerformanceMetricsEvent[]
6
+ addSnapshot: (snapshot: PerformanceMetricsEvent) => void
7
+ }
8
+
9
+ const MAX = 60
10
+
11
+ export const useDashboardStore = create<DashboardStore>((set) => ({
12
+ snapshots: [],
13
+ addSnapshot: (snapshot) =>
14
+ set((s) => ({ snapshots: [...s.snapshots, snapshot].slice(-MAX) })),
15
+ }))
16
+
17
+ export const useDashboardSnapshots = () => useDashboardStore((s) => s.snapshots)
18
+ export const useLatestSnapshot = () =>
19
+ useDashboardStore((s) => s.snapshots[s.snapshots.length - 1] ?? null)
@@ -0,0 +1 @@
1
+ export { MetricRow } from './metric-row'
@@ -0,0 +1,25 @@
1
+ import { Box, Text } from 'ink'
2
+ import { Gauge } from '../../../../../shared/components/gauge/index.js'
3
+ import { Sparkline } from '../../../../../shared/components/sparkline/index.js'
4
+
5
+ interface MetricRowProps {
6
+ label: string
7
+ value: number
8
+ max: number
9
+ unit: string
10
+ values: number[]
11
+ sparkWidth: number
12
+ warnAt?: number
13
+ critAt?: number
14
+ }
15
+
16
+ export function MetricRow({ label, value, max, unit, values, sparkWidth, warnAt, critAt }: MetricRowProps) {
17
+ return (
18
+ <Box gap={1}>
19
+ <Text color="gray">{label}</Text>
20
+ <Gauge value={value} max={max} width={12} warnAt={warnAt} critAt={critAt} />
21
+ <Sparkline values={values} max={max} width={sparkWidth} />
22
+ <Text>{String(Math.round(value)).padStart(4)}{unit}</Text>
23
+ </Box>
24
+ )
25
+ }
@@ -0,0 +1 @@
1
+ export { PerformancePanel } from './performance-panel'
@@ -0,0 +1,67 @@
1
+ import { Text } from "ink";
2
+ import type { PerformanceMetricsEvent } from "@salve-software/salvetron-types";
3
+ import { Panel } from "../../../../../shared/components/panel/index.js";
4
+ import { MetricRow } from "../metric-row/index.js";
5
+
6
+ interface PerformancePanelProps {
7
+ latest: PerformanceMetricsEvent | null;
8
+ snapshots: PerformanceMetricsEvent[];
9
+ sparkWidth: number;
10
+ height: number;
11
+ }
12
+
13
+ export function PerformancePanel({
14
+ latest,
15
+ snapshots,
16
+ sparkWidth,
17
+ height,
18
+ }: PerformancePanelProps) {
19
+ return (
20
+ <Panel title="Performance" height={height}>
21
+ {latest ? (
22
+ <>
23
+ <MetricRow
24
+ label="UI FPS"
25
+ value={latest.uiFps}
26
+ max={60}
27
+ unit="fps"
28
+ values={snapshots.map((s) => s.uiFps)}
29
+ sparkWidth={sparkWidth}
30
+ />
31
+ <MetricRow
32
+ label="JS FPS"
33
+ value={latest.jsFps}
34
+ max={60}
35
+ unit="fps"
36
+ values={snapshots.map((s) => s.jsFps)}
37
+ sparkWidth={sparkWidth}
38
+ />
39
+ <MetricRow
40
+ label="RAM "
41
+ value={latest.memoryUsage}
42
+ max={512}
43
+ unit="MB"
44
+ values={snapshots.map((s) => s.memoryUsage)}
45
+ sparkWidth={sparkWidth}
46
+ warnAt={0.6}
47
+ critAt={0.85}
48
+ />
49
+ <MetricRow
50
+ label="CPU "
51
+ value={latest.cpuUsage}
52
+ max={100}
53
+ unit="%"
54
+ values={snapshots.map((s) => s.cpuUsage)}
55
+ sparkWidth={sparkWidth}
56
+ warnAt={0.6}
57
+ critAt={0.8}
58
+ />
59
+ </>
60
+ ) : (
61
+ <Text color="gray" dimColor>
62
+ Waiting for performance data...
63
+ </Text>
64
+ )}
65
+ </Panel>
66
+ );
67
+ }