dashcam 1.0.1-beta.5 → 1.0.1-beta.7
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/BACKWARD_COMPATIBILITY.md +177 -0
- package/bin/dashcam-background.js +177 -0
- package/bin/dashcam.js +265 -139
- package/lib/logs/index.js +67 -11
- package/lib/processManager.js +61 -33
- package/lib/recorder.js +20 -5
- package/lib/tracking/FileTracker.js +7 -0
- package/lib/tracking/LogsTracker.js +21 -7
- package/lib/uploader.js +7 -4
- package/package.json +4 -1
- package/test_workflow.sh +99 -25
- package/.github/workflows/build.yml +0 -103
- package/.github/workflows/release.yml +0 -107
package/bin/dashcam.js
CHANGED
|
@@ -26,7 +26,7 @@ if (!fs.existsSync(APP.recordingsDir)) {
|
|
|
26
26
|
|
|
27
27
|
program
|
|
28
28
|
.name('dashcam')
|
|
29
|
-
.description('
|
|
29
|
+
.description('Capture the steps to reproduce every bug.')
|
|
30
30
|
.version(APP.version)
|
|
31
31
|
.option('-v, --verbose', 'Enable verbose logging output')
|
|
32
32
|
.hook('preAction', (thisCommand) => {
|
|
@@ -39,8 +39,8 @@ program
|
|
|
39
39
|
|
|
40
40
|
program
|
|
41
41
|
.command('auth')
|
|
42
|
-
.description(
|
|
43
|
-
.argument('<
|
|
42
|
+
.description("Authenticate the dashcam desktop using a team's apiKey")
|
|
43
|
+
.argument('<api-key>', 'Your team API key')
|
|
44
44
|
.action(async (apiKey, options, command) => {
|
|
45
45
|
try {
|
|
46
46
|
logger.verbose('Starting authentication process', {
|
|
@@ -75,9 +75,160 @@ program
|
|
|
75
75
|
}
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
// Shared recording action to avoid duplication
|
|
79
|
+
async function recordingAction(options, command) {
|
|
80
|
+
try {
|
|
81
|
+
const silent = options.silent;
|
|
82
|
+
const log = (...args) => { if (!silent) console.log(...args); };
|
|
83
|
+
const logError = (...args) => { if (!silent) console.error(...args); };
|
|
84
|
+
|
|
85
|
+
// Check if recording is already active
|
|
86
|
+
if (processManager.isRecordingActive()) {
|
|
87
|
+
const status = processManager.getActiveStatus();
|
|
88
|
+
const duration = ((Date.now() - status.startTime) / 1000).toFixed(1);
|
|
89
|
+
log('Recording already in progress');
|
|
90
|
+
log(`Duration: ${duration} seconds`);
|
|
91
|
+
log(`PID: ${status.pid}`);
|
|
92
|
+
log('Use "dashcam stop" to stop the recording');
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check authentication
|
|
97
|
+
if (!await auth.isAuthenticated()) {
|
|
98
|
+
log('You need to login first. Run: dashcam auth <api-key>');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check for piped input (description from stdin) if description option not set
|
|
103
|
+
let description = options.description;
|
|
104
|
+
if (!description && !process.stdin.isTTY) {
|
|
105
|
+
const chunks = [];
|
|
106
|
+
for await (const chunk of process.stdin) {
|
|
107
|
+
chunks.push(chunk);
|
|
108
|
+
}
|
|
109
|
+
description = Buffer.concat(chunks).toString('utf-8');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check screen recording permissions (macOS only)
|
|
113
|
+
const { ensurePermissions } = await import('../lib/permissions.js');
|
|
114
|
+
const hasPermissions = await ensurePermissions();
|
|
115
|
+
if (!hasPermissions) {
|
|
116
|
+
log('\n⚠️ Cannot start recording without screen recording permission.');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Start recording in background mode
|
|
121
|
+
log('Starting recording in background...');
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const result = await processManager.startRecording({
|
|
125
|
+
fps: parseInt(options.fps) || 30,
|
|
126
|
+
audio: options.audio,
|
|
127
|
+
output: options.output,
|
|
128
|
+
title: options.title,
|
|
129
|
+
description: description,
|
|
130
|
+
project: options.project || options.k // Support both -p and -k for project
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
log(`✅ Recording started successfully (PID: ${result.pid})`);
|
|
134
|
+
log(`Output: ${result.outputPath}`);
|
|
135
|
+
log('');
|
|
136
|
+
log('Use "dashcam status" to check progress');
|
|
137
|
+
log('Use "dashcam stop" to stop recording and upload');
|
|
138
|
+
|
|
139
|
+
process.exit(0);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
logError('Failed to start recording:', error.message);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
logger.error('Failed to start recording:', error);
|
|
146
|
+
if (!options.silent) console.error('Failed to start recording:', error.message);
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 'create' command - creates a clip from current recording (like stop but with more options)
|
|
152
|
+
program
|
|
153
|
+
.command('create')
|
|
154
|
+
.description('Create a clip and output the resulting url or markdown. Will launch desktop app for local editing before publishing.')
|
|
155
|
+
.option('-t, --title <string>', 'Title of the replay. Automatically generated if not supplied.')
|
|
156
|
+
.option('-d, --description [text]', 'Replay markdown body. This may also be piped in: `cat README.md | dashcam create`')
|
|
157
|
+
.option('--md', 'Returns code for a rich markdown image link.')
|
|
158
|
+
.option('-k, --project <project>', 'Project ID to publish to')
|
|
159
|
+
.action(async (options) => {
|
|
160
|
+
try {
|
|
161
|
+
// Check for piped input (description from stdin)
|
|
162
|
+
let description = options.description;
|
|
163
|
+
if (!description && !process.stdin.isTTY) {
|
|
164
|
+
const chunks = [];
|
|
165
|
+
for await (const chunk of process.stdin) {
|
|
166
|
+
chunks.push(chunk);
|
|
167
|
+
}
|
|
168
|
+
description = Buffer.concat(chunks).toString('utf-8');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!processManager.isRecordingActive()) {
|
|
172
|
+
console.log('No active recording to create clip from');
|
|
173
|
+
console.log('Start a recording first with "dashcam record" or "dashcam start"');
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const activeStatus = processManager.getActiveStatus();
|
|
178
|
+
|
|
179
|
+
console.log('Creating clip from recording...');
|
|
180
|
+
|
|
181
|
+
const result = await processManager.stopActiveRecording();
|
|
182
|
+
|
|
183
|
+
if (!result) {
|
|
184
|
+
console.log('Failed to stop recording');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log('Recording stopped successfully');
|
|
189
|
+
|
|
190
|
+
// Upload the recording
|
|
191
|
+
console.log('Uploading clip...');
|
|
192
|
+
try {
|
|
193
|
+
const uploadResult = await upload(result.outputPath, {
|
|
194
|
+
title: options.title || activeStatus?.options?.title || 'Dashcam Recording',
|
|
195
|
+
description: description || activeStatus?.options?.description,
|
|
196
|
+
project: options.project || options.k || activeStatus?.options?.project,
|
|
197
|
+
duration: result.duration,
|
|
198
|
+
clientStartDate: result.clientStartDate,
|
|
199
|
+
apps: result.apps,
|
|
200
|
+
icons: result.icons,
|
|
201
|
+
gifPath: result.gifPath,
|
|
202
|
+
snapshotPath: result.snapshotPath
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// Output based on format option
|
|
206
|
+
if (options.md) {
|
|
207
|
+
const replayId = uploadResult.replay?.id;
|
|
208
|
+
const shareKey = uploadResult.shareLink.split('share=')[1];
|
|
209
|
+
console.log(`[](${uploadResult.shareLink})`);
|
|
210
|
+
console.log('');
|
|
211
|
+
console.log(`Watch [Dashcam - ${options.title || 'New Replay'}](${uploadResult.shareLink}) on Dashcam`);
|
|
212
|
+
} else {
|
|
213
|
+
console.log(uploadResult.shareLink);
|
|
214
|
+
}
|
|
215
|
+
} catch (uploadError) {
|
|
216
|
+
console.error('Upload failed:', uploadError.message);
|
|
217
|
+
console.log('Recording saved locally:', result.outputPath);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
process.exit(0);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
logger.error('Error creating clip:', error);
|
|
223
|
+
console.error('Failed to create clip:', error.message);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// 'record' command - the main recording command with all options
|
|
78
229
|
program
|
|
79
230
|
.command('record')
|
|
80
|
-
.description('Start a
|
|
231
|
+
.description('Start a recording terminal to be included in your dashcam video recording')
|
|
81
232
|
.option('-a, --audio', 'Include audio in the recording')
|
|
82
233
|
.option('-f, --fps <fps>', 'Frames per second (default: 30)', '30')
|
|
83
234
|
.option('-o, --output <path>', 'Custom output path')
|
|
@@ -85,123 +236,38 @@ program
|
|
|
85
236
|
.option('-d, --description <description>', 'Description for the recording (supports markdown)')
|
|
86
237
|
.option('-p, --project <project>', 'Project ID to upload the recording to')
|
|
87
238
|
.option('-s, --silent', 'Silent mode - suppress all output')
|
|
88
|
-
.action(
|
|
89
|
-
try {
|
|
90
|
-
const silent = options.silent;
|
|
91
|
-
const log = (...args) => { if (!silent) console.log(...args); };
|
|
92
|
-
const logError = (...args) => { if (!silent) console.error(...args); };
|
|
93
|
-
|
|
94
|
-
// Check if recording is already active
|
|
95
|
-
if (processManager.isRecordingActive()) {
|
|
96
|
-
const status = processManager.getActiveStatus();
|
|
97
|
-
const duration = ((Date.now() - status.startTime) / 1000).toFixed(1);
|
|
98
|
-
log('Recording already in progress');
|
|
99
|
-
log(`Duration: ${duration} seconds`);
|
|
100
|
-
log(`PID: ${status.pid}`);
|
|
101
|
-
log('Use "dashcam stop" to stop the recording');
|
|
102
|
-
process.exit(0);
|
|
103
|
-
}
|
|
239
|
+
.action(recordingAction);
|
|
104
240
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
241
|
+
program
|
|
242
|
+
.command('pipe')
|
|
243
|
+
.description('Pipe command output to dashcam to be included in recorded video')
|
|
244
|
+
.action(async () => {
|
|
245
|
+
try {
|
|
246
|
+
// Check if recording is active
|
|
247
|
+
if (!processManager.isRecordingActive()) {
|
|
248
|
+
console.error('No active recording. Start a recording first with "dashcam record" or "dashcam start"');
|
|
108
249
|
process.exit(1);
|
|
109
250
|
}
|
|
110
251
|
|
|
111
|
-
//
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
process.
|
|
252
|
+
// Read from stdin
|
|
253
|
+
const chunks = [];
|
|
254
|
+
for await (const chunk of process.stdin) {
|
|
255
|
+
chunks.push(chunk);
|
|
256
|
+
// Also output to stdout so pipe continues to work
|
|
257
|
+
process.stdout.write(chunk);
|
|
117
258
|
}
|
|
259
|
+
const content = Buffer.concat(chunks).toString('utf-8');
|
|
118
260
|
|
|
119
|
-
//
|
|
120
|
-
|
|
261
|
+
// Import the log tracker to add the piped content
|
|
262
|
+
const { logsTrackerManager } = await import('../lib/logs/index.js');
|
|
121
263
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
output: options.output,
|
|
127
|
-
title: options.title,
|
|
128
|
-
description: options.description,
|
|
129
|
-
project: options.project
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
log(`Recording started successfully (PID: ${result.pid})`);
|
|
133
|
-
log(`Output: ${result.outputPath}`);
|
|
134
|
-
log('Use "dashcam status" to check progress');
|
|
135
|
-
log('Use "dashcam stop" to stop recording and upload');
|
|
136
|
-
|
|
137
|
-
// Keep this process alive for background recording
|
|
138
|
-
log('Recording is running in background...');
|
|
139
|
-
|
|
140
|
-
// Set up signal handlers for graceful shutdown
|
|
141
|
-
let isShuttingDown = false;
|
|
142
|
-
const handleShutdown = async (signal) => {
|
|
143
|
-
if (isShuttingDown) {
|
|
144
|
-
log('Shutdown already in progress...');
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
isShuttingDown = true;
|
|
148
|
-
|
|
149
|
-
log(`\nReceived ${signal}, stopping background recording...`);
|
|
150
|
-
try {
|
|
151
|
-
// Stop the recording using the recorder directly (not processManager)
|
|
152
|
-
const { stopRecording } = await import('../lib/recorder.js');
|
|
153
|
-
const stopResult = await stopRecording();
|
|
154
|
-
|
|
155
|
-
if (stopResult) {
|
|
156
|
-
log('Recording stopped:', stopResult.outputPath);
|
|
157
|
-
|
|
158
|
-
// Import and call upload function with the correct format
|
|
159
|
-
const { upload } = await import('../lib/uploader.js');
|
|
160
|
-
|
|
161
|
-
log('Starting upload...');
|
|
162
|
-
const uploadResult = await upload(stopResult.outputPath, {
|
|
163
|
-
title: options.title || 'Dashcam Recording',
|
|
164
|
-
description: options.description || 'Recorded with Dashcam CLI',
|
|
165
|
-
project: options.project,
|
|
166
|
-
duration: stopResult.duration,
|
|
167
|
-
clientStartDate: stopResult.clientStartDate,
|
|
168
|
-
apps: stopResult.apps,
|
|
169
|
-
logs: stopResult.logs,
|
|
170
|
-
gifPath: stopResult.gifPath,
|
|
171
|
-
snapshotPath: stopResult.snapshotPath
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// Write upload result for stop command to read
|
|
175
|
-
processManager.writeUploadResult({
|
|
176
|
-
shareLink: uploadResult.shareLink,
|
|
177
|
-
replayId: uploadResult.replay?.id
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
log('✅ Upload complete!');
|
|
181
|
-
log('📹 Watch your recording:', uploadResult.shareLink);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Clean up process files, but preserve upload result for stop command
|
|
185
|
-
processManager.cleanup({ preserveResult: true });
|
|
186
|
-
} catch (error) {
|
|
187
|
-
logError('Error during shutdown:', error.message);
|
|
188
|
-
logger.error('Error during shutdown:', error);
|
|
189
|
-
}
|
|
190
|
-
process.exit(0);
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
194
|
-
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
195
|
-
|
|
196
|
-
// Keep the process alive
|
|
197
|
-
await new Promise(() => {});
|
|
198
|
-
} catch (error) {
|
|
199
|
-
logError('Failed to start recording:', error.message);
|
|
200
|
-
process.exit(1);
|
|
201
|
-
}
|
|
264
|
+
// Add piped content as a log entry
|
|
265
|
+
logsTrackerManager.addPipedLog(content);
|
|
266
|
+
|
|
267
|
+
process.exit(0);
|
|
202
268
|
} catch (error) {
|
|
203
|
-
logger.error('Failed to
|
|
204
|
-
|
|
269
|
+
logger.error('Failed to pipe content:', error);
|
|
270
|
+
console.error('Failed to pipe content:', error.message);
|
|
205
271
|
process.exit(1);
|
|
206
272
|
}
|
|
207
273
|
});
|
|
@@ -228,43 +294,94 @@ program
|
|
|
228
294
|
|
|
229
295
|
|
|
230
296
|
|
|
297
|
+
// 'start' command - alias for record with simple instant replay mode
|
|
298
|
+
program
|
|
299
|
+
.command('start')
|
|
300
|
+
.description('Start instant replay recording on dashcam')
|
|
301
|
+
.action(async () => {
|
|
302
|
+
// Call recordingAction with minimal options for instant replay
|
|
303
|
+
await recordingAction({
|
|
304
|
+
fps: '30',
|
|
305
|
+
audio: false,
|
|
306
|
+
silent: false
|
|
307
|
+
}, null);
|
|
308
|
+
});
|
|
309
|
+
|
|
231
310
|
program
|
|
232
311
|
.command('track')
|
|
233
|
-
.description('
|
|
234
|
-
.option('--
|
|
235
|
-
.option('--
|
|
236
|
-
.option('--
|
|
312
|
+
.description('Add a logs config to Dashcam')
|
|
313
|
+
.option('--name <name>', 'Name for the tracking configuration (required)')
|
|
314
|
+
.option('--type <type>', 'Type of tracker: "application" or "web" (required)')
|
|
315
|
+
.option('--pattern <pattern>', 'Pattern to track (can be used multiple times)', (value, previous) => {
|
|
316
|
+
return previous ? previous.concat([value]) : [value];
|
|
317
|
+
})
|
|
318
|
+
.option('--web <pattern>', 'Web URL pattern to track (can use wildcards like *) - deprecated, use --type=web --pattern instead')
|
|
319
|
+
.option('--app <pattern>', 'Application file pattern to track (can use wildcards like *) - deprecated, use --type=application --pattern instead')
|
|
237
320
|
.action(async (options) => {
|
|
238
321
|
try {
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
322
|
+
// Support both old and new syntax
|
|
323
|
+
// New syntax: --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"
|
|
324
|
+
// Old syntax: --web <pattern> --app <pattern>
|
|
325
|
+
|
|
326
|
+
if (options.type && options.pattern) {
|
|
327
|
+
// New syntax validation
|
|
328
|
+
if (!options.name) {
|
|
329
|
+
console.error('Error: --name is required when using --type and --pattern');
|
|
330
|
+
console.log('Example: dashcam track --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (options.type !== 'web' && options.type !== 'application') {
|
|
335
|
+
console.error('Error: --type must be either "web" or "application"');
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
252
338
|
|
|
253
|
-
await createPattern(config);
|
|
254
|
-
console.log('Web tracking pattern added successfully:', options.web);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (options.app) {
|
|
258
339
|
const config = {
|
|
259
|
-
name: options.name
|
|
260
|
-
type:
|
|
261
|
-
patterns:
|
|
340
|
+
name: options.name,
|
|
341
|
+
type: options.type,
|
|
342
|
+
patterns: options.pattern,
|
|
262
343
|
enabled: true
|
|
263
344
|
};
|
|
264
345
|
|
|
265
346
|
await createPattern(config);
|
|
266
|
-
console.log('Application tracking pattern added successfully
|
|
347
|
+
console.log(`${options.type === 'web' ? 'Web' : 'Application'} tracking pattern added successfully:`, options.name);
|
|
348
|
+
console.log('Patterns:', options.pattern.join(', '));
|
|
349
|
+
|
|
350
|
+
} else if (options.web || options.app) {
|
|
351
|
+
// Old syntax for backward compatibility
|
|
352
|
+
if (options.web) {
|
|
353
|
+
const config = {
|
|
354
|
+
name: options.name || 'Web Pattern',
|
|
355
|
+
type: 'web',
|
|
356
|
+
patterns: [options.web],
|
|
357
|
+
enabled: true
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
await createPattern(config);
|
|
361
|
+
console.log('Web tracking pattern added successfully:', options.web);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (options.app) {
|
|
365
|
+
const config = {
|
|
366
|
+
name: options.name || 'App Pattern',
|
|
367
|
+
type: 'application',
|
|
368
|
+
patterns: [options.app],
|
|
369
|
+
enabled: true
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
await createPattern(config);
|
|
373
|
+
console.log('Application tracking pattern added successfully:', options.app);
|
|
374
|
+
}
|
|
375
|
+
} else {
|
|
376
|
+
console.error('Error: Must provide either:');
|
|
377
|
+
console.log(' --name --type --pattern (new syntax)');
|
|
378
|
+
console.log(' --web or --app (old syntax)');
|
|
379
|
+
console.log('\nExamples:');
|
|
380
|
+
console.log(' dashcam track --name=social --type=web --pattern="*facebook.com*" --pattern="*twitter.com*"');
|
|
381
|
+
console.log(' dashcam track --web "*facebook.com*"');
|
|
382
|
+
process.exit(1);
|
|
267
383
|
}
|
|
384
|
+
|
|
268
385
|
process.exit(0);
|
|
269
386
|
} catch (error) {
|
|
270
387
|
console.error('Failed to add tracking pattern:', error.message);
|
|
@@ -566,4 +683,13 @@ program
|
|
|
566
683
|
}
|
|
567
684
|
});
|
|
568
685
|
|
|
686
|
+
// If no command specified and there are options like --md, treat as create command
|
|
687
|
+
program.action(async (options) => {
|
|
688
|
+
// Default to create command when running just "dashcam"
|
|
689
|
+
const createCommand = program.commands.find(cmd => cmd.name() === 'create');
|
|
690
|
+
if (createCommand && createCommand._actionHandler) {
|
|
691
|
+
await createCommand._actionHandler(options);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
|
|
569
695
|
program.parse();
|
package/lib/logs/index.js
CHANGED
|
@@ -15,7 +15,13 @@ const CLI_CONFIG_FILE = path.join(process.cwd(), '.dashcam', 'cli-config.json');
|
|
|
15
15
|
|
|
16
16
|
// Simple trim function for CLI (adapted from desktop app)
|
|
17
17
|
async function trimLogs(groupLogStatuses, startMS, endMS, clientStartDate, clipId) {
|
|
18
|
-
logger.info('Trimming logs', {
|
|
18
|
+
logger.info('Trimming logs', {
|
|
19
|
+
count: groupLogStatuses.length,
|
|
20
|
+
startMS,
|
|
21
|
+
endMS,
|
|
22
|
+
clientStartDate,
|
|
23
|
+
clientStartDateReadable: new Date(clientStartDate).toISOString()
|
|
24
|
+
});
|
|
19
25
|
|
|
20
26
|
const REPLAY_DIR = path.join(os.tmpdir(), 'dashcam', 'recordings');
|
|
21
27
|
|
|
@@ -36,12 +42,24 @@ async function trimLogs(groupLogStatuses, startMS, endMS, clientStartDate, clipI
|
|
|
36
42
|
let events = content;
|
|
37
43
|
|
|
38
44
|
// Convert events to relative time
|
|
39
|
-
let relativeEvents = events.map((event) => {
|
|
45
|
+
let relativeEvents = events.map((event, index) => {
|
|
46
|
+
const originalTime = event.time;
|
|
40
47
|
event.time = parseInt(event.time + '') - startMS;
|
|
41
48
|
// Check if it's not already relative time
|
|
42
49
|
if (event.time > 1_000_000_000_000) {
|
|
43
50
|
// relative time = absolute time - clip start time
|
|
44
51
|
event.time = event.time - clientStartDate;
|
|
52
|
+
if (index === 0) {
|
|
53
|
+
// Log first event for debugging
|
|
54
|
+
logger.info('First event timestamp conversion', {
|
|
55
|
+
originalTime,
|
|
56
|
+
clientStartDate,
|
|
57
|
+
relativeTime: event.time,
|
|
58
|
+
relativeSeconds: (event.time / 1000).toFixed(2),
|
|
59
|
+
startMS,
|
|
60
|
+
line: event.line ? event.line.substring(0, 50) : 'N/A'
|
|
61
|
+
});
|
|
62
|
+
}
|
|
45
63
|
}
|
|
46
64
|
return event;
|
|
47
65
|
});
|
|
@@ -56,16 +74,19 @@ async function trimLogs(groupLogStatuses, startMS, endMS, clientStartDate, clipI
|
|
|
56
74
|
});
|
|
57
75
|
|
|
58
76
|
if (status.type === 'cli') {
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
logFile: name,
|
|
67
|
-
};
|
|
77
|
+
// For CLI logs, keep the file paths in logFile field for UI display
|
|
78
|
+
// No need to remap to numeric indices
|
|
79
|
+
|
|
80
|
+
// Create items array showing unique files and their event counts
|
|
81
|
+
const fileCounts = {};
|
|
82
|
+
filteredEvents.forEach(event => {
|
|
83
|
+
fileCounts[event.logFile] = (fileCounts[event.logFile] || 0) + 1;
|
|
68
84
|
});
|
|
85
|
+
|
|
86
|
+
status.items = Object.entries(fileCounts).map(([filePath, count]) => ({
|
|
87
|
+
item: filePath,
|
|
88
|
+
count
|
|
89
|
+
}));
|
|
69
90
|
}
|
|
70
91
|
} else if (status.type === 'web' && !webHandled) {
|
|
71
92
|
logger.debug('Found web groupLog, handling all web groupLogs at once');
|
|
@@ -243,6 +264,41 @@ class LogsTrackerManager {
|
|
|
243
264
|
this.removeCliTrackedPath(filePath);
|
|
244
265
|
}
|
|
245
266
|
|
|
267
|
+
addPipedLog(content) {
|
|
268
|
+
// Find active recording instance
|
|
269
|
+
const activeInstances = Object.values(this.instances);
|
|
270
|
+
if (activeInstances.length === 0) {
|
|
271
|
+
logger.warn('No active recording to add piped content to');
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Add to the most recent active instance (last one)
|
|
276
|
+
const instance = activeInstances[activeInstances.length - 1];
|
|
277
|
+
const timestamp = Date.now();
|
|
278
|
+
|
|
279
|
+
// Write to CLI tracker's temp file
|
|
280
|
+
if (instance.trackers && instance.trackers.cli) {
|
|
281
|
+
const pipedLog = {
|
|
282
|
+
time: timestamp,
|
|
283
|
+
logFile: 'piped-input',
|
|
284
|
+
content: content
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Write to the CLI tracker's output file
|
|
288
|
+
try {
|
|
289
|
+
const outputFile = path.join(instance.directory, 'cli-logs.jsonl');
|
|
290
|
+
const logLine = JSON.stringify(pipedLog) + '\n';
|
|
291
|
+
fs.appendFileSync(outputFile, logLine);
|
|
292
|
+
logger.info('Added piped content to recording', {
|
|
293
|
+
recorderId: instance.recorderId,
|
|
294
|
+
contentLength: content.length
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
logger.error('Failed to write piped content', { error });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
246
302
|
removeTracker(id) {
|
|
247
303
|
// Try removing from web trackers first
|
|
248
304
|
if (this.webLogsConfig[id]) {
|