Phase 3: Session recording/playback and encrypted profile export/import
Session Recording & Playback: - Add recording package with asciicast v2 format writer and reader - Recorder captures terminal I/O with timestamps from SSH and Telnet sessions - Input and output events recorded via hooks in session manager callbacks - Recording store manages .cast files in ~/Library/Application Support/NexusTerminal/recordings - Recording browser modal lists all saved recordings with metadata - Full playback viewer with embedded xterm.js terminal, progress scrubber, play/pause/restart controls, and 0.5x-10x speed adjustment - Recording indicator (pulsing red dot) shown on active tab during capture - Keyboard shortcut Cmd+Shift+R to toggle recording on active session - Tests for recording, store listing, deletion, and double-close safety Encrypted Profile Export/Import: - Add exportimport package with AES-256-GCM encryption (Argon2id key derivation) - File format: version byte + 32-byte salt + 12-byte nonce + ciphertext - Same Argon2id parameters as vault (128 MiB, 3 iterations, 4 threads) - Export modal with profile selection checkboxes and password entry - Import modal with password entry and conflict strategy (skip/overwrite/duplicate) - Native save/open file dialogs for .nexus files - Tests for round-trip encryption, wrong password rejection, corrupt files - Replaced placeholder import/export with full encrypted implementation UI: - Added export and recordings buttons to sidebar footer - Recording and playback CSS with charcoal design tokens - Wired RecordingManager and ExportImportUI into app.js
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
/* ── Session Recording & Playback ────────────────────────────────────── */
|
||||
|
||||
/* Recording indicator dot on tabs */
|
||||
.recording-dot {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--error, #ef4444);
|
||||
margin-right: 6px;
|
||||
flex-shrink: 0;
|
||||
animation: recording-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes recording-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
/* Recordings list in browser modal */
|
||||
.recordings-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.recording-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: var(--surface-2, #2a2a2e);
|
||||
border-radius: 6px;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.recording-item:hover {
|
||||
background: var(--surface-3, #333338);
|
||||
}
|
||||
|
||||
.recording-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.recording-title {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary, #e4e4e7);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.recording-meta {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary, #71717a);
|
||||
}
|
||||
|
||||
.recording-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* Playback modal — larger to fit a terminal */
|
||||
.modal-playback {
|
||||
width: 90vw;
|
||||
max-width: 900px;
|
||||
max-height: 90vh;
|
||||
}
|
||||
|
||||
.playback-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.playback-terminal {
|
||||
flex: 1;
|
||||
min-height: 300px;
|
||||
background: #1a1a1e;
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.playback-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px 12px;
|
||||
background: var(--surface-1, #1e1e22);
|
||||
border-top: 1px solid var(--border, #3f3f46);
|
||||
}
|
||||
|
||||
.playback-progress {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background: var(--surface-3, #333338);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.playback-progress::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent, #3b82f6);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.playback-time {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary, #71717a);
|
||||
white-space: nowrap;
|
||||
min-width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.playback-speed {
|
||||
font-size: 11px;
|
||||
background: var(--surface-2, #2a2a2e);
|
||||
color: var(--text-primary, #e4e4e7);
|
||||
border: 1px solid var(--border, #3f3f46);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Export/import profile list */
|
||||
.export-profile-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
background: var(--surface-2, #2a2a2e);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border, #3f3f46);
|
||||
}
|
||||
|
||||
.export-profile-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary, #e4e4e7);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background 0.1s ease;
|
||||
}
|
||||
|
||||
.export-profile-item:hover {
|
||||
background: var(--surface-3, #333338);
|
||||
}
|
||||
|
||||
.export-profile-item input[type="checkbox"] {
|
||||
accent-color: var(--accent, #3b82f6);
|
||||
}
|
||||
Reference in New Issue
Block a user