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.
- package/README.md +333 -0
- package/bin/salvetron.tsx +9 -0
- package/package.json +43 -0
- package/scripts/salvetron-sim.mjs +75 -0
- package/src/app.tsx +62 -0
- package/src/modules/dashboard/store/dashboard.store.ts +19 -0
- package/src/modules/dashboard/ui/components/metric-row/index.ts +1 -0
- package/src/modules/dashboard/ui/components/metric-row/metric-row.tsx +25 -0
- package/src/modules/dashboard/ui/components/performance-panel/index.ts +1 -0
- package/src/modules/dashboard/ui/components/performance-panel/performance-panel.tsx +67 -0
- package/src/modules/dashboard/ui/containers/dashboard-container/dashboard-container.tsx +290 -0
- package/src/modules/dashboard/ui/containers/dashboard-container/index.ts +1 -0
- package/src/modules/js-logs/store/js-logs.store.ts +21 -0
- package/src/modules/js-logs/ui/components/log-detail/index.ts +1 -0
- package/src/modules/js-logs/ui/components/log-detail/log-detail.tsx +62 -0
- package/src/modules/js-logs/ui/components/log-list/index.ts +1 -0
- package/src/modules/js-logs/ui/components/log-list/log-list.tsx +32 -0
- package/src/modules/js-logs/ui/containers/js-logs-container/index.ts +1 -0
- package/src/modules/js-logs/ui/containers/js-logs-container/js-logs-container.tsx +80 -0
- package/src/modules/native-logs/store/native-logs.store.ts +21 -0
- package/src/modules/native-logs/ui/components/native-log-detail/index.ts +1 -0
- package/src/modules/native-logs/ui/components/native-log-detail/native-log-detail.tsx +63 -0
- package/src/modules/native-logs/ui/components/native-log-list/index.ts +1 -0
- package/src/modules/native-logs/ui/components/native-log-list/native-log-list.tsx +33 -0
- package/src/modules/native-logs/ui/containers/native-logs-container/index.ts +1 -0
- package/src/modules/native-logs/ui/containers/native-logs-container/native-logs-container.tsx +80 -0
- package/src/modules/network/library/constants.ts +15 -0
- package/src/modules/network/store/network.store.ts +58 -0
- package/src/modules/network/ui/components/network-detail/index.ts +1 -0
- package/src/modules/network/ui/components/network-detail/network-detail.tsx +82 -0
- package/src/modules/network/ui/components/network-row/index.ts +1 -0
- package/src/modules/network/ui/components/network-row/network-row.tsx +23 -0
- package/src/modules/network/ui/components/network-table-header/index.ts +1 -0
- package/src/modules/network/ui/components/network-table-header/network-table-header.tsx +12 -0
- package/src/modules/network/ui/containers/network-container/index.ts +1 -0
- package/src/modules/network/ui/containers/network-container/network-container.tsx +93 -0
- package/src/server/ws-server.ts +52 -0
- package/src/shared/components/ascii-logo/ascii-logo.tsx +169 -0
- package/src/shared/components/ascii-logo/ascii.txt +0 -0
- package/src/shared/components/ascii-logo/index.ts +1 -0
- package/src/shared/components/gauge/gauge.tsx +18 -0
- package/src/shared/components/gauge/index.ts +1 -0
- package/src/shared/components/log-row/index.ts +1 -0
- package/src/shared/components/log-row/log-row.tsx +28 -0
- package/src/shared/components/panel/index.ts +1 -0
- package/src/shared/components/panel/panel.tsx +28 -0
- package/src/shared/components/sparkline/index.ts +1 -0
- package/src/shared/components/sparkline/sparkline.tsx +26 -0
- package/src/shared/components/status-bar/index.ts +1 -0
- package/src/shared/components/status-bar/status-bar.tsx +32 -0
- package/src/shared/components/tab-bar/index.ts +1 -0
- package/src/shared/components/tab-bar/tab-bar.tsx +36 -0
- package/src/shared/hooks/use-detail-panel.ts +78 -0
- package/src/shared/hooks/use-list-navigation.ts +44 -0
- package/src/shared/hooks/use-terminal-size.ts +42 -0
- package/src/shared/store/device.store.ts +25 -0
- package/src/shared/types.ts +3 -0
- package/src/shared/utils/build-curl-command.ts +19 -0
- package/src/shared/utils/clipboard.ts +27 -0
- 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
|
+
}
|