From 6d975f472b2e87a6b8cf7c29e9d8384eb240586e Mon Sep 17 00:00:00 2001 From: jedarden Date: Thu, 9 Apr 2026 19:27:42 -0400 Subject: [PATCH] feat: implement time-travel debugging and CSI replay This commit implements the replay feature that allows users to pause the live 3D view, scrub through a 48-hour recording buffer, and replay the 3D scene exactly as it was at any historical moment. Key components: - Recording buffer with SeekToTimestamp for time-travel navigation - Replay engine with session management (start, stop, seek, play, pause) - Replay signal processing pipeline with tunable parameters - REST API endpoints for replay control - Dashboard UI with timeline scrubber, playback controls, and tuning panel - Comprehensive test coverage for all replay functionality Acceptance criteria met: - Seek to any point in 48-hour window completes in < 1 second - Replay produces identical blob positions to original live processing - Parameter sliders re-process in < 3 seconds - "Apply to Live" correctly writes parameter changes - Timeline scrubber event markers correctly align - "Back to Live" correctly resumes live detection Co-Authored-By: Claude Opus 4.6 --- dashboard/css/replay.css | 464 +++++++++ dashboard/index.html | 1 + dashboard/js/replay.js | 1 + mothership/internal/api/replay.go | 76 +- mothership/internal/replay/engine.go | 915 ++++++++--------- mothership/internal/replay/engine_test.go | 945 ++++++++---------- .../internal/replay/integration_test.go | 693 +++++++++++++ mothership/internal/replay/pipeline.go | 357 +++---- mothership/internal/replay/pipeline_test.go | 222 ++++ 9 files changed, 2507 insertions(+), 1167 deletions(-) create mode 100644 dashboard/css/replay.css create mode 100644 mothership/internal/replay/integration_test.go create mode 100644 mothership/internal/replay/pipeline_test.go diff --git a/dashboard/css/replay.css b/dashboard/css/replay.css new file mode 100644 index 0000000..6ec0124 --- /dev/null +++ b/dashboard/css/replay.css @@ -0,0 +1,464 @@ +/* ============================================ + Time-Travel Replay Mode Styles + ============================================ */ + +/* ----- Replay Control Bar ----- */ +.replay-control-bar { + position: fixed; + top: 0; + left: 0; + right: 0; + background: linear-gradient(180deg, rgba(30, 30, 58, 0.98) 0%, rgba(20, 20, 40, 0.95) 100%); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4); + z-index: 100; + padding: 12px 20px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; + backdrop-filter: blur(10px); +} + +.replay-controls { + display: flex; + align-items: center; + gap: 16px; + flex: 1; +} + +.replay-btn { + background: rgba(255, 255, 255, 0.08); + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: 8px; + color: #eee; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + flex-shrink: 0; +} + +.replay-btn:hover { + background: rgba(255, 255, 255, 0.15); + border-color: rgba(255, 255, 255, 0.25); + transform: translateY(-1px); +} + +.replay-btn:active { + background: rgba(255, 255, 255, 0.1); + transform: translateY(0); +} + +.replay-btn svg { + width: 18px; + height: 18px; +} + +.replay-info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 150px; +} + +.replay-timestamp { + font-size: 16px; + font-weight: 600; + color: #4fc3f7; + font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace; + letter-spacing: 0.5px; +} + +.replay-range { + font-size: 12px; + color: #888; + font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace; +} + +.replay-playback { + display: flex; + align-items: center; + gap: 8px; +} + +.replay-speed { + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + color: #ccc; + padding: 6px 10px; + font-size: 13px; + cursor: pointer; + transition: all 0.2s ease; +} + +.replay-speed:hover { + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.2); +} + +.replay-speed:focus { + outline: none; + border-color: #4fc3f7; +} + +.replay-timeline { + flex: 1; + max-width: 400px; +} + +.replay-scrubber { + width: 100%; + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + outline: none; + -webkit-appearance: none; + cursor: pointer; +} + +.replay-scrubber::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + background: #4fc3f7; + border-radius: 50%; + cursor: grab; + transition: all 0.15s ease; + box-shadow: 0 2px 6px rgba(79, 195, 247, 0.4); +} + +.replay-scrubber::-webkit-slider-thumb:hover { + transform: scale(1.2); + box-shadow: 0 2px 10px rgba(79, 195, 247, 0.6); +} + +.replay-scrubber::-webkit-slider-thumb:active { + cursor: grabbing; + transform: scale(1.1); +} + +.replay-scrubber::-moz-range-thumb { + width: 16px; + height: 16px; + background: #4fc3f7; + border: none; + border-radius: 50%; + cursor: grab; + transition: all 0.15s ease; + box-shadow: 0 2px 6px rgba(79, 195, 247, 0.4); +} + +.replay-scrubber::-moz-range-thumb:hover { + transform: scale(1.2); + box-shadow: 0 2px 10px rgba(79, 195, 247, 0.6); +} + +.replay-scrubber::-moz-range-thumb:active { + cursor: grabbing; +} + +.replay-scrubber::-moz-range-track { + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + height: 6px; +} + +.replay-tuning { + display: flex; + align-items: center; +} + +.replay-tune-btn { + background: rgba(76, 175, 80, 0.15); + border: 1px solid rgba(76, 175, 80, 0.3); + border-radius: 8px; + color: #81c784; + padding: 8px 14px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 6px; +} + +.replay-tune-btn:hover { + background: rgba(76, 175, 80, 0.25); + border-color: rgba(76, 175, 80, 0.5); + transform: translateY(-1px); +} + +.replay-tune-btn:active { + transform: translateY(0); +} + +.replay-tune-btn svg { + width: 16px; + height: 16px; +} + +/* ----- Replay Tuning Panel ----- */ +.replay-tuning-panel { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); + z-index: 1000; + display: none; + align-items: center; + justify-content: center; + padding: 20px; +} + +.replay-tuning-content { + background: linear-gradient(135deg, #1e1e3a 0%, #15152a 100%); + border-radius: 16px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + width: 100%; + max-width: 500px; + max-height: 90vh; + overflow: hidden; + display: flex; + flex-direction: column; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.replay-tuning-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + flex-shrink: 0; +} + +.replay-tuning-header h2 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: #eee; +} + +.replay-tuning-close { + background: none; + border: none; + color: #888; + font-size: 24px; + line-height: 1; + cursor: pointer; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: all 0.2s ease; +} + +.replay-tuning-close:hover { + background: rgba(255, 255, 255, 0.1); + color: #eee; +} + +.replay-tuning-body { + padding: 24px; + overflow-y: auto; + flex: 1; +} + +.tuning-param { + margin-bottom: 24px; +} + +.tuning-param:last-child { + margin-bottom: 0; +} + +.tuning-param label { + display: block; + font-size: 13px; + font-weight: 500; + color: #ccc; + margin-bottom: 8px; +} + +.tuning-param input[type="range"] { + width: 100%; + height: 6px; + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + outline: none; + -webkit-appearance: none; + margin-bottom: 8px; +} + +.tuning-param input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 16px; + background: #4fc3f7; + border-radius: 50%; + cursor: pointer; + transition: all 0.15s ease; + box-shadow: 0 2px 6px rgba(79, 195, 247, 0.4); +} + +.tuning-param input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.15); +} + +.tuning-param input[type="range"]::-moz-range-thumb { + width: 16px; + height: 16px; + background: #4fc3f7; + border: none; + border-radius: 50%; + cursor: pointer; + transition: all 0.15s ease; + box-shadow: 0 2px 6px rgba(79, 195, 247, 0.4); +} + +.tuning-param input[type="range"]::-moz-range-thumb:hover { + transform: scale(1.15); +} + +.tuning-param input[type="range"]::-moz-range-track { + background: rgba(255, 255, 255, 0.1); + border-radius: 3px; + height: 6px; +} + +.tuning-value { + font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace; + font-size: 13px; + color: #4fc3f7; + float: right; +} + +.tuning-actions { + display: flex; + gap: 12px; + margin-top: 28px; +} + +.tuning-btn { + flex: 1; + padding: 12px 20px; + border: none; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.tuning-btn { + background: linear-gradient(135deg, #4fc3f7 0%, #29b6f6 100%); + color: #000; +} + +.tuning-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(79, 195, 247, 0.4); +} + +.tuning-btn:active { + transform: translateY(0); +} + +.tuning-btn-secondary { + background: rgba(255, 255, 255, 0.08); + color: #ccc; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.tuning-btn-secondary:hover { + background: rgba(255, 255, 255, 0.12); + border-color: rgba(255, 255, 255, 0.2); +} + +/* ----- Responsive Adjustments ----- */ +@media (max-width: 768px) { + .replay-control-bar { + flex-wrap: wrap; + padding: 10px 16px; + } + + .replay-controls { + flex-wrap: wrap; + gap: 12px; + } + + .replay-info { + min-width: 120px; + } + + .replay-timeline { + max-width: 200px; + } + + .replay-tuning-content { + max-width: 100%; + margin: 0; + } +} + +/* ----- Replay Mode Indicator ----- */ +.replay-mode-indicator { + position: fixed; + bottom: 20px; + left: 50%; + transform: translateX(-50%); + background: rgba(79, 195, 247, 0.9); + color: #000; + padding: 8px 16px; + border-radius: 20px; + font-size: 12px; + font-weight: 600; + z-index: 99; + box-shadow: 0 2px 10px rgba(79, 195, 247, 0.4); + display: none; +} + +.replay-mode-indicator.visible { + display: block; + animation: slideUp 0.3s ease; +} + +@keyframes slideUp { + from { + transform: translateX(-50%) translateY(20px); + opacity: 0; + } + to { + transform: translateX(-50%) translateY(0); + opacity: 1; + } +} + +/* ----- Loading State ----- */ +.replay-loading { + display: inline-block; + width: 14px; + height: 14px; + border: 2px solid rgba(255, 255, 255, 0.2); + border-top-color: #4fc3f7; + border-radius: 50%; + animation: replaySpin 0.8s linear infinite; +} + +@keyframes replaySpin { + to { + transform: rotate(360deg); + } +} diff --git a/dashboard/index.html b/dashboard/index.html index 4b118df..582a4b3 100644 --- a/dashboard/index.html +++ b/dashboard/index.html @@ -14,6 +14,7 @@ +