style(dashboard): continue design token migration across remaining CSS

Replace additional hard-coded colors with design tokens in layout,
notifications, panels, timeline, and other CSS files.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-04-24 16:41:18 -04:00
parent 07abc03ef4
commit 1e8876d6b4
21 changed files with 664 additions and 220 deletions

View file

@ -103,7 +103,7 @@ body.ambient-mode {
/* ===== Ambient Status Line ===== */
.ambient-status {
position: fixed;
bottom: 20px;
bottomvar(--space-5);
left: 50%;
transform: translateX(-50%);
display: flex;
@ -118,7 +118,7 @@ body.ambient-mode {
.ambient-status-item {
display: flex;
align-items: center;
gap: 6px;
gapvar(--space-150);
}
.ambient-status-dot {
@ -363,7 +363,7 @@ body.ambient-mode {
}
.ambient-alert-btn {
padding: 14px 28px;
paddingvar(--space-350) var(--space-7);
border-radius: var(--radius-card);
font-size: var(--text-base);
font-weight: 600;
@ -491,7 +491,7 @@ body.ambient-mode {
/* ===== Responsive Design ===== */
@media (max-width: 768px) {
.ambient-container {
padding: 10px;
paddingvar(--space-250);
}
.ambient-person {

View file

@ -267,7 +267,7 @@
#feedback-notes-input {
width: 100%;
min-height: 80px;
padding: 10px;
paddingvar(--space-250);
background: var(--bg-input);
border: 1px solid var(--slate-6);
border-radius: var(--radius-control);
@ -325,7 +325,7 @@
#anomaly-learning-banner {
position: fixed;
top: 60px;
topvar(--space-16);
left: 50%;
transform: translateX(-50%);
z-index: 500;
@ -385,7 +385,7 @@
#security-mode-indicator {
display: flex;
align-items: center;
gap: 6px;
gapvar(--space-150);
padding: var(--space-1) 10px;
border-radius: var(--radius-modal);
font-size: var(--text-xs);
@ -442,7 +442,7 @@
background: none;
border: none;
cursor: pointer;
padding: 2px;
paddingvar(--space-half);
margin-left: var(--space-1);
border-radius: var(--radius-control);
color: inherit;
@ -775,7 +775,7 @@
.anomaly-history-score {
font-size: var(--text-xs);
padding: 2px 6px;
paddingvar(--space-half) var(--space-150);
border-radius: var(--radius-control);
font-weight: 600;
flex-shrink: 0;
@ -798,7 +798,7 @@
.anomaly-history-feedback {
font-size: var(--text-xs);
padding: 2px 6px;
paddingvar(--space-half) var(--space-150);
border-radius: var(--radius-control);
flex-shrink: 0;
}

View file

@ -86,7 +86,7 @@
.ble-device {
display: flex;
align-items: center;
gap: 10px;
gapvar(--space-250);
padding: 10px var(--space-3);
background: var(--border-subtle);
border-radius: var(--radius-control);
@ -214,7 +214,7 @@
.ble-form-group input[type="color"] {
height: 36px;
padding: 2px;
paddingvar(--space-half);
cursor: pointer;
}

View file

@ -2,7 +2,7 @@
#briefing-card {
position: fixed;
top: 60px;
topvar(--space-16);
left: 50%;
transform: translateX(-50%);
width: 90%;
@ -162,7 +162,7 @@
/* Simple mode briefing card */
.simple-mode #briefing-card {
top: auto;
bottom: 20px;
bottomvar(--space-5);
transform: translateX(-50%);
}
@ -185,7 +185,7 @@
#briefing-indicator {
position: fixed;
top: 50%;
right: 20px;
rightvar(--space-5);
transform: translateY(-50%);
width: 40px;
height: 40px;
@ -279,8 +279,8 @@
#briefing-settings .setting-toggle::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
topvar(--space-half);
leftvar(--space-half);
width: 20px;
height: 20px;
background: var(--text-on-accent);

View file

@ -62,7 +62,7 @@
.cp-search-row {
display: flex;
align-items: center;
gap: 10px;
gapvar(--space-250);
padding: 14px var(--space-4);
border-bottom: 1px solid var(--border-default);
}
@ -93,7 +93,7 @@
color: var(--slate-7);
background: var(--highlight-subtle);
border-radius: var(--radius-control);
padding: 2px 7px;
paddingvar(--space-half) var(--space-175);
flex-shrink: 0;
font-family: var(--font-mono);
}
@ -143,7 +143,7 @@
.cp-item {
display: flex;
align-items: center;
gap: 10px;
gapvar(--space-250);
padding: 9px var(--space-4);
cursor: pointer;
transition: background 0.1s;

View file

@ -52,8 +52,8 @@
.toggle-switch .slider:before {
width: 36px;
height: 36px;
top: 4px;
left: 4px;
topvar(--space-1);
leftvar(--space-1);
}
.toggle-switch input:checked + .slider:before {

View file

@ -198,7 +198,7 @@
.zone-badge {
display: inline-block;
min-width: 20px;
padding: 2px 6px;
paddingvar(--space-half) var(--space-150);
border-radius: var(--radius-control);
font-size: var(--text-2xs);
font-weight: 600;
@ -248,7 +248,7 @@
.ble-match-header {
display: flex;
align-items: center;
gap: 10px;
gapvar(--space-250);
margin-bottom: 10px;
}
@ -274,7 +274,7 @@
.ble-match-details {
display: flex;
flex-direction: column;
gap: 6px;
gapvar(--space-150);
}
.ble-match-detail {

View file

@ -66,7 +66,7 @@ body.fleet-page {
.nav-btn {
display: flex;
align-items: center;
gap: 6px;
gapvar(--space-150);
padding: var(--space-2) var(--space-4);
background: transparent;
border: 1px solid var(--border-default);
@ -116,7 +116,7 @@ body.fleet-page {
.summary-item {
display: flex;
gap: 6px;
gapvar(--space-150);
}
.summary-label {
@ -143,7 +143,7 @@ body.fleet-page {
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
gapvar(--space-150);
padding: var(--space-2) var(--space-4);
border-radius: var(--radius-control);
font-size: var(--text-sm);
@ -267,7 +267,7 @@ body.fleet-page {
.filter-chip {
display: inline-flex;
align-items: center;
gap: 6px;
gapvar(--space-150);
padding: var(--space-1) 10px;
background: var(--blue-muted);
border: 1px solid var(--blue-border);
@ -509,7 +509,7 @@ body.fleet-page {
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
gapvar(--space-150);
padding: var(--space-1) 10px;
border-radius: var(--radius-card);
font-size: var(--text-xs);
@ -559,7 +559,7 @@ body.fleet-page {
.firmware-version {
display: flex;
align-items: center;
gap: 6px;
gapvar(--space-150);
}
.firmware-current {
@ -574,7 +574,7 @@ body.fleet-page {
display: inline-flex;
align-items: center;
gap: var(--space-1);
padding: 2px 6px;
paddingvar(--space-half) var(--space-150);
background: var(--warn-bg);
border-radius: var(--radius-control);
font-size: var(--text-2xs);
@ -694,7 +694,7 @@ body.fleet-page {
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 6px;
paddingvar(--space-150);
font-size: var(--text-base);
border-radius: var(--radius-control);
transition: all 0.2s;
@ -981,8 +981,8 @@ body.fleet-page {
============================================ */
.toast-container {
position: fixed;
bottom: 24px;
right: 24px;
bottomvar(--space-6);
rightvar(--space-6);
z-index: 1100;
display: flex;
flex-direction: column;
@ -1163,9 +1163,9 @@ body.fleet-page {
}
.toast-container {
right: 12px;
left: 12px;
bottom: 12px;
rightvar(--space-3);
leftvar(--space-3);
bottomvar(--space-3);
}
.toast {

View file

@ -4,7 +4,7 @@
.floorplan-panel {
position: fixed;
top: 60px;
topvar(--space-16);
right: 340px;
width: 380px;
max-height: calc(100vh - 80px);
@ -73,7 +73,7 @@
}
.floorplan-btn {
padding: 6px 14px;
paddingvar(--space-150) var(--space-350);
border-radius: var(--radius-control);
font-size: var(--text-xs);
cursor: pointer;
@ -81,7 +81,7 @@
border: none;
display: inline-flex;
align-items: center;
gap: 6px;
gapvar(--space-150);
}
.floorplan-btn-primary {
@ -213,7 +213,7 @@
.floorplan-distance-input input {
width: 100%;
padding: 6px 10px;
paddingvar(--space-150) var(--space-250);
background: var(--border-default);
border: 1px solid var(--border-strong);
border-radius: var(--radius-control);
@ -255,7 +255,7 @@
/* Responsive adjustments */
@media (max-width: 1200px) {
.floorplan-panel {
right: 20px;
rightvar(--space-5);
width: 340px;
}
}

View file

@ -7,8 +7,8 @@
/* ===== Guided Help Container ===== */
.guided-help-container {
position: fixed;
bottom: 20px;
left: 20px;
bottomvar(--space-5);
leftvar(--space-5);
z-index: 200;
pointer-events: none;
opacity: 0;
@ -46,7 +46,7 @@
.help-title {
display: flex;
align-items: center;
gap: 10px;
gapvar(--space-250);
}
.help-icon {
@ -269,9 +269,9 @@
/* ===== Responsive Design ===== */
@media (max-width: 480px) {
.guided-help-container {
bottom: 10px;
left: 10px;
right: 10px;
bottomvar(--space-250);
leftvar(--space-250);
rightvar(--space-250);
}
.guided-help-panel {

View file

@ -124,7 +124,7 @@
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
gapvar(--space-half);
padding: var(--space-2) 0;
color: var(--text-muted);
text-decoration: none;
@ -269,8 +269,8 @@
.live-panel--left {
position: fixed;
top: 60px;
left: 20px;
topvar(--space-16);
leftvar(--space-5);
width: 280px;
max-height: calc(100vh - 80px);
padding: var(--space-3);
@ -278,16 +278,16 @@
.live-panel--right {
position: fixed;
bottom: 20px;
right: 20px;
bottomvar(--space-5);
rightvar(--space-5);
width: 400px;
padding: var(--space-3);
}
.live-panel--presence {
position: fixed;
top: 60px;
right: 20px;
topvar(--space-16);
rightvar(--space-5);
width: 240px;
max-height: calc(100vh - 80px);
padding: var(--space-3);
@ -296,8 +296,8 @@
/* Legacy ID-based selectors (match live.html panel IDs) */
#node-panel {
position: fixed;
top: 60px;
left: 20px;
topvar(--space-16);
leftvar(--space-5);
width: 280px;
max-height: calc(100vh - 80px);
background: var(--live-node-bg);
@ -309,8 +309,8 @@
#chart-panel {
position: fixed;
bottom: 20px;
right: 20px;
bottomvar(--space-5);
rightvar(--space-5);
width: 400px;
height: 260px;
background: var(--live-chart-bg);
@ -321,8 +321,8 @@
#presence-panel {
position: fixed;
top: 60px;
right: 20px;
topvar(--space-16);
rightvar(--space-5);
width: 300px;
background: var(--live-panel-bg);
border-radius: var(--radius-card);
@ -342,8 +342,8 @@
#chart-panel {
width: calc(100% - 16px);
right: 8px;
left: 8px;
rightvar(--space-2);
leftvar(--space-2);
bottom: calc(8px + env(safe-area-inset-bottom));
}
}
@ -352,9 +352,9 @@
@media (max-width: 639px) {
#chart-panel {
width: calc(100% - 16px);
right: 8px;
left: 8px;
bottom: 8px;
rightvar(--space-2);
leftvar(--space-2);
bottomvar(--space-2);
height: 200px;
}
}
@ -392,13 +392,13 @@ dialog.app-dialog::backdrop {
/* ── Setup toolbar (floats over 3D canvas in setup page) ── */
.setup-toolbar {
position: fixed;
bottom: 20px;
bottomvar(--space-5);
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
gapvar(--space-2);
background: var(--overlay-strong);
padding: 8px 16px;
paddingvar(--space-2) var(--space-4);
border-radius: var(--radius-card);
z-index: 100;
}
@ -407,7 +407,7 @@ dialog.app-dialog::backdrop {
background: var(--slate-5);
border: 1px solid var(--border-default);
color: var(--text-primary);
padding: 8px 16px;
paddingvar(--space-2) var(--space-4);
border-radius: var(--radius-control);
cursor: pointer;
font-size: var(--text-sm);
@ -428,8 +428,8 @@ dialog.app-dialog::backdrop {
@media (max-width: 639px) {
.setup-toolbar {
bottom: calc(8px + env(safe-area-inset-bottom));
left: 8px;
right: 8px;
leftvar(--space-2);
rightvar(--space-2);
transform: none;
width: auto;
overflow-x: auto;
@ -438,7 +438,7 @@ dialog.app-dialog::backdrop {
.setup-toolbar button {
flex-shrink: 0;
padding: 8px 12px;
paddingvar(--space-2) var(--space-3);
font-size: var(--text-xs);
}
}
@ -473,8 +473,8 @@ dialog.app-dialog::backdrop {
#diurnal-banner {
top: auto;
bottom: calc(60px + env(safe-area-inset-bottom));
left: 8px;
right: 8px;
leftvar(--space-2);
rightvar(--space-2);
transform: none;
}
}
@ -492,8 +492,8 @@ dialog.app-dialog::backdrop {
#anomaly-learning-banner {
top: auto;
bottom: calc(60px + env(safe-area-inset-bottom));
left: 8px;
right: 8px;
leftvar(--space-2);
rightvar(--space-2);
transform: none;
}
}
@ -502,7 +502,7 @@ dialog.app-dialog::backdrop {
#security-mode-indicator {
position: fixed;
top: 45px;
right: 20px;
rightvar(--space-5);
z-index: 150;
}
@ -510,21 +510,21 @@ dialog.app-dialog::backdrop {
#security-mode-indicator {
top: auto;
bottom: calc(60px + env(safe-area-inset-bottom));
right: 8px;
rightvar(--space-2);
}
}
/* ── Link health panel (floats over canvas) ── */
.link-health-panel {
position: fixed;
top: 60px;
right: 20px;
topvar(--space-16);
rightvar(--space-5);
z-index: 100;
}
@media (max-width: 1023px) {
.link-health-panel {
right: 8px;
rightvar(--space-2);
}
}
@ -532,8 +532,8 @@ dialog.app-dialog::backdrop {
.link-health-panel {
top: auto;
bottom: calc(60px + env(safe-area-inset-bottom));
left: 8px;
right: 8px;
leftvar(--space-2);
rightvar(--space-2);
max-height: 40vh;
overflow-y: auto;
}
@ -542,7 +542,7 @@ dialog.app-dialog::backdrop {
/* ── Replay control bar (shown during replay) ── */
.replay-control-bar {
position: fixed;
bottom: 20px;
bottomvar(--space-5);
left: 50%;
transform: translateX(-50%);
z-index: 150;
@ -550,8 +550,8 @@ dialog.app-dialog::backdrop {
@media (max-width: 639px) {
.replay-control-bar {
left: 8px;
right: 8px;
leftvar(--space-2);
rightvar(--space-2);
transform: none;
bottom: calc(60px + env(safe-area-inset-bottom));
}
@ -560,7 +560,7 @@ dialog.app-dialog::backdrop {
/* ── Replay tuning panel (floats over canvas) ── */
.replay-tuning-panel {
position: fixed;
top: 60px;
topvar(--space-16);
right: 340px;
width: 280px;
z-index: 100;
@ -569,8 +569,8 @@ dialog.app-dialog::backdrop {
@media (max-width: 1023px) {
.replay-tuning-panel {
right: 8px;
left: 8px;
rightvar(--space-2);
leftvar(--space-2);
width: auto;
max-width: 340px;
}
@ -578,8 +578,8 @@ dialog.app-dialog::backdrop {
@media (max-width: 639px) {
.replay-tuning-panel {
right: 8px;
left: 8px;
rightvar(--space-2);
leftvar(--space-2);
width: auto;
top: auto;
bottom: calc(60px + env(safe-area-inset-bottom));
@ -599,7 +599,7 @@ dialog.app-dialog::backdrop {
/* ── Portal editor panel (floats over canvas) ── */
#portal-editor-panel {
position: fixed;
top: 60px;
topvar(--space-16);
left: 320px;
width: 240px;
z-index: 100;
@ -608,8 +608,8 @@ dialog.app-dialog::backdrop {
@media (max-width: 1023px) {
#portal-editor-panel {
left: 8px;
right: 8px;
leftvar(--space-2);
rightvar(--space-2);
width: auto;
max-width: 300px;
}
@ -617,8 +617,8 @@ dialog.app-dialog::backdrop {
@media (max-width: 639px) {
#portal-editor-panel {
left: 8px;
right: 8px;
leftvar(--space-2);
rightvar(--space-2);
width: auto;
top: auto;
bottom: calc(60px + env(safe-area-inset-bottom));
@ -630,7 +630,7 @@ dialog.app-dialog::backdrop {
/* ── Room editor panel (floats over canvas) ── */
#room-editor-panel {
position: fixed;
top: 60px;
topvar(--space-16);
left: 320px;
width: 240px;
z-index: 100;
@ -639,8 +639,8 @@ dialog.app-dialog::backdrop {
@media (max-width: 1023px) {
#room-editor-panel {
left: 8px;
right: 8px;
leftvar(--space-2);
rightvar(--space-2);
width: auto;
max-width: 300px;
}
@ -648,8 +648,8 @@ dialog.app-dialog::backdrop {
@media (max-width: 639px) {
#room-editor-panel {
left: 8px;
right: 8px;
leftvar(--space-2);
rightvar(--space-2);
width: auto;
top: auto;
bottom: calc(60px + env(safe-area-inset-bottom));
@ -661,7 +661,7 @@ dialog.app-dialog::backdrop {
/* ── GDOP legend (floats over canvas) ── */
#gdop-legend {
position: fixed;
bottom: 20px;
bottomvar(--space-5);
left: 320px;
z-index: 100;
display: none;
@ -669,14 +669,14 @@ dialog.app-dialog::backdrop {
@media (max-width: 1023px) {
#gdop-legend {
left: 8px;
leftvar(--space-2);
bottom: calc(280px + env(safe-area-inset-bottom));
}
}
@media (max-width: 639px) {
#gdop-legend {
left: 8px;
leftvar(--space-2);
bottom: calc(220px + env(safe-area-inset-bottom));
}
}

View file

@ -33,7 +33,7 @@
/* Channel Cards */
.channels-list {
display: grid;
gap: 15px;
gapvar(--space-375);
margin-bottom: var(--space-5);
}
@ -41,7 +41,7 @@
background: var(--bg-hover);
border: 1px solid var(--slate-5);
border-radius: var(--radius-card);
padding: 15px;
paddingvar(--space-375);
}
.channel-header {
@ -60,7 +60,7 @@
.channel-actions {
display: flex;
gap: 10px;
gapvar(--space-250);
}
.channel-details {
@ -109,7 +109,7 @@
border: none;
font-size: var(--text-xl);
cursor: pointer;
padding: 5px;
paddingvar(--space-125);
opacity: 0.7;
transition: opacity 0.2s;
}
@ -128,7 +128,7 @@
.checkbox-label {
display: flex;
align-items: center;
gap: 10px;
gapvar(--space-250);
color: var(--text-on-accent);
cursor: pointer;
}
@ -147,7 +147,7 @@
.time-field {
display: flex;
flex-direction: column;
gap: 5px;
gapvar(--space-125);
}
.time-field label {
@ -167,18 +167,18 @@
.days-selector {
display: flex;
flex-direction: column;
gap: 10px;
gapvar(--space-250);
}
.day-checkbox {
display: flex;
gap: 15px;
gapvar(--space-375);
}
.day-checkbox-label {
display: flex;
align-items: center;
gap: 5px;
gapvar(--space-125);
color: var(--text-on-accent);
cursor: pointer;
font-size: 0.9em;
@ -194,14 +194,14 @@
.batching-config {
display: flex;
flex-direction: column;
gap: 15px;
gapvar(--space-375);
}
.batch-window,
.max-batch-size {
display: flex;
flex-direction: column;
gap: 5px;
gapvar(--space-125);
}
.batch-window label,
@ -231,8 +231,8 @@
.event-type-toggle {
display: flex;
align-items: center;
gap: 15px;
padding: 10px;
gapvar(--space-375);
paddingvar(--space-250);
background: var(--bg-hover);
border-radius: var(--radius-control);
}
@ -283,8 +283,8 @@
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
leftvar(--space-075);
bottomvar(--space-075);
background-color: var(--text-on-accent);
transition: 0.3s;
border-radius: 50%;
@ -344,7 +344,7 @@
color: var(--text-muted);
font-size: var(--text-3xl);
cursor: pointer;
padding: 5px;
paddingvar(--space-125);
line-height: 1;
}
@ -382,7 +382,7 @@
.auth-fields {
display: flex;
gap: 10px;
gapvar(--space-250);
}
.auth-fields input {
@ -403,12 +403,12 @@
#modal-event-types {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
gapvar(--space-250);
}
.form-actions {
display: flex;
gap: 10px;
gapvar(--space-250);
justify-content: flex-end;
margin-top: var(--space-5);
}
@ -416,8 +416,8 @@
/* Toast Notifications */
.toast {
position: fixed;
bottom: 20px;
right: 20px;
bottomvar(--space-5);
rightvar(--space-5);
padding: 15px var(--space-5);
border-radius: var(--radius-card);
color: var(--text-on-accent);
@ -466,16 +466,16 @@
/* Responsive Design */
@media (max-width: 600px) {
.notification-settings {
padding: 15px;
paddingvar(--space-375);
}
.settings-section {
padding: 15px;
paddingvar(--space-375);
}
.time-range {
flex-direction: column;
gap: 10px;
gapvar(--space-250);
}
.channels-list {
@ -551,7 +551,7 @@
.channel-config-group {
margin-bottom: var(--space-5);
padding: 15px;
paddingvar(--space-375);
background: var(--bg-hover);
border-radius: var(--radius-control);
}
@ -575,14 +575,14 @@
.panel-days-selector {
display: flex;
flex-wrap: wrap;
gap: 10px;
gapvar(--space-250);
margin-top: 10px;
}
.panel-day-checkbox {
display: flex;
align-items: center;
gap: 5px;
gapvar(--space-125);
color: var(--text-on-accent);
cursor: pointer;
font-size: 0.9em;
@ -597,8 +597,8 @@
.panel-event-type-toggle {
display: flex;
align-items: center;
gap: 15px;
padding: 10px;
gapvar(--space-375);
paddingvar(--space-250);
background: var(--bg-hover);
border-radius: var(--radius-control);
}
@ -648,8 +648,8 @@
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
leftvar(--space-075);
bottomvar(--space-075);
background-color: var(--text-on-accent);
transition: 0.3s;
border-radius: 50%;

View file

@ -263,12 +263,12 @@
/* ----- Toast Notifications ----- */
#toast-container {
position: fixed;
bottom: 20px;
right: 20px;
bottomvar(--space-5);
rightvar(--space-5);
z-index: 3000;
display: flex;
flex-direction: column;
gap: 10px;
gapvar(--space-250);
pointer-events: none;
}
@ -385,7 +385,7 @@
/* ----- Mode Toggle Bar ----- */
.mode-toggle-bar {
position: fixed;
top: 40px;
topvar(--space-10);
left: 0;
right: 0;
height: 44px;
@ -426,8 +426,8 @@
content: '';
position: absolute;
bottom: 0;
left: 16px;
right: 16px;
leftvar(--space-4);
rightvar(--space-4);
height: 2px;
background: var(--blue-10);
border-radius: 1px 1px 0 0;
@ -498,7 +498,7 @@
background: var(--bg-hover);
border-radius: var(--radius-control);
outline: none;
margin: 10px;
marginvar(--space-250);
}
.panel-form-group input[type="range"]::-webkit-slider-thumb {
@ -538,7 +538,7 @@
.panel-form-checkbox {
display: flex;
align-items: center;
gap: 10px;
gapvar(--space-250);
cursor: pointer;
padding: 6px 0;
}
@ -627,7 +627,7 @@
.panel-btn-group {
display: flex;
gap: 10px;
gapvar(--space-250);
margin-top: var(--space-4);
}
@ -828,7 +828,7 @@
color: var(--warn);
display: flex;
align-items: center;
gap: 10px;
gapvar(--space-250);
}
#security-dialog .learning-warning::before {
@ -900,7 +900,7 @@
.alert-banner-actions {
display: flex;
gap: 10px;
gapvar(--space-250);
}
.alert-banner-btn {
@ -1027,7 +1027,7 @@
.learning-ready-badge {
display: inline-flex;
align-items: center;
gap: 6px;
gapvar(--space-150);
padding: var(--space-1) 10px;
background: var(--ok-muted);
border-radius: var(--radius-card);
@ -1091,7 +1091,7 @@
/* Arm/Disarm Button */
.security-arm-btn {
width: 100%;
padding: 14px;
paddingvar(--space-350);
border-radius: var(--radius-card);
font-size: var(--text-md);
font-weight: 600;
@ -1101,7 +1101,7 @@
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
gapvar(--space-250);
}
.security-arm-btn.disarmed {
@ -1257,9 +1257,9 @@
}
#toast-container {
left: 10px;
right: 10px;
bottom: 10px;
leftvar(--space-250);
rightvar(--space-250);
bottomvar(--space-250);
}
.toast {
@ -1671,7 +1671,7 @@
.contributing-badge {
display: inline-block;
padding: 2px 6px;
paddingvar(--space-half) var(--space-150);
border-radius: var(--radius-control);
font-size: var(--text-2xs);
font-weight: 600;
@ -1702,7 +1702,7 @@
.ble-match-header {
display: flex;
align-items: center;
gap: 10px;
gapvar(--space-250);
margin-bottom: var(--space-3);
}
@ -1732,7 +1732,7 @@
.ble-match-details {
display: flex;
flex-direction: column;
gap: 6px;
gapvar(--space-150);
}
.ble-match-detail {
@ -1787,7 +1787,7 @@
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
gapvar(--space-250);
font-size: var(--text-sm);
color: var(--slate-11);
transition: background 0.2s, color 0.2s;
@ -1845,7 +1845,7 @@
.links-table th,
.links-table td {
padding: 6px 6px;
paddingvar(--space-150) var(--space-150);
}
}
@ -1868,7 +1868,7 @@
.volume-tools {
display: flex;
gap: 6px;
gapvar(--space-150);
align-items: center;
}
@ -1927,7 +1927,7 @@
.trigger-card-actions {
display: flex;
gap: 6px;
gapvar(--space-150);
align-items: center;
flex-shrink: 0;
}
@ -1935,8 +1935,8 @@
.error-badge {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 7px;
gapvar(--space-075);
paddingvar(--space-half) var(--space-175);
font-size: var(--text-3xs);
font-weight: 600;
border-radius: var(--radius-control);
@ -1949,8 +1949,8 @@
.disabled-badge {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 7px;
gapvar(--space-075);
paddingvar(--space-half) var(--space-175);
font-size: var(--text-3xs);
font-weight: 600;
border-radius: var(--radius-control);
@ -1963,8 +1963,8 @@
.warning-badge {
display: inline-flex;
align-items: center;
gap: 3px;
padding: 2px 7px;
gapvar(--space-075);
paddingvar(--space-half) var(--space-175);
font-size: var(--text-3xs);
font-weight: 600;
border-radius: var(--radius-control);
@ -2070,8 +2070,8 @@
position: absolute;
width: 16px;
height: 16px;
left: 2px;
bottom: 2px;
leftvar(--space-half);
bottomvar(--space-half);
background: var(--slate-11);
border-radius: 50%;
transition: transform 0.2s, background 0.2s;
@ -2125,7 +2125,7 @@
.form-control {
width: 100%;
padding: 6px 10px;
paddingvar(--space-150) var(--space-250);
font-size: var(--text-sm);
background: var(--border-subtle);
border: 1px solid var(--bg-active);
@ -2382,7 +2382,7 @@
/* Simulator Panel */
.simulator-panel {
position: fixed;
top: 40px;
topvar(--space-10);
right: 0;
bottom: 0;
width: 360px;
@ -2459,7 +2459,7 @@
.sim-space-controls {
display: flex;
flex-direction: column;
gap: 10px;
gapvar(--space-250);
margin-bottom: var(--space-3);
}
@ -2473,7 +2473,7 @@
.sim-space-controls input[type="number"] {
width: 80px;
padding: 6px 10px;
paddingvar(--space-150) var(--space-250);
background: var(--border-subtle);
border: 1px solid var(--border-strong);
border-radius: var(--radius-control);
@ -2491,7 +2491,7 @@
.sim-tool-btn {
flex: 1;
padding: 10px;
paddingvar(--space-250);
background: var(--border-subtle);
border: 1px solid var(--border-strong);
border-radius: var(--radius-control);
@ -2645,7 +2645,7 @@
.sim-gdop-controls label {
display: flex;
align-items: center;
gap: 6px;
gapvar(--space-150);
font-size: var(--text-sm);
color: var(--slate-11);
cursor: pointer;
@ -2732,7 +2732,7 @@
.sim-recommendation {
display: flex;
align-items: flex-start;
gap: 10px;
gapvar(--space-250);
padding: 10px var(--space-3);
background: var(--border-subtle);
border-radius: var(--radius-control);
@ -3044,7 +3044,7 @@
align-items: center;
font-size: var(--text-sm);
color: var(--text-muted);
gap: 6px;
gapvar(--space-150);
}
.sim-gdop-controls input[type="checkbox"] {
@ -3178,7 +3178,7 @@
}
.sim-rec-priority {
padding: 2px 6px;
paddingvar(--space-half) var(--space-150);
border-radius: var(--radius-control);
font-size: var(--text-3xs);
font-weight: 600;
@ -3272,7 +3272,7 @@
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 10px;
gapvar(--space-250);
}
.gdop-stat-item {

View file

@ -61,7 +61,7 @@
.replay-info {
display: flex;
flex-direction: column;
gap: 2px;
gapvar(--space-half);
min-width: 150px;
}
@ -90,7 +90,7 @@
border: 1px solid var(--bg-hover);
border-radius: var(--radius-control);
color: var(--slate-11);
padding: 6px 10px;
paddingvar(--space-150) var(--space-250);
font-size: var(--text-sm);
cursor: pointer;
transition: all 0.2s ease;
@ -185,7 +185,7 @@
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 6px;
gapvar(--space-150);
}
.replay-tune-btn:hover {
@ -416,7 +416,7 @@
/* ----- Replay Mode Indicator ----- */
.replay-mode-indicator {
position: fixed;
bottom: 20px;
bottomvar(--space-5);
left: 50%;
transform: translateX(-50%);
background: var(--blue-10);

View file

@ -348,7 +348,7 @@
.alert-banner-actions {
display: flex;
gap: 10px;
gapvar(--space-250);
flex-shrink: 0;
}
@ -390,7 +390,7 @@
#security-mode-indicator {
position: fixed;
top: 45px;
right: 20px;
rightvar(--space-5);
background: var(--alert);
color: var(--text-on-accent);
padding: 6px var(--space-3);
@ -468,7 +468,7 @@
font-size: var(--text-sm);
display: none;
align-items: center;
gap: 10px;
gapvar(--space-250);
z-index: 150;
box-shadow: 0 4px 12px var(--blue-border);
}

View file

@ -348,7 +348,7 @@ body.simple-mode {
.simple-room-card .room-status {
display: flex;
align-items: center;
gap: 6px;
gapvar(--space-150);
font-size: var(--text-sm);
font-weight: 500;
padding: var(--space-1) 10px;
@ -373,7 +373,7 @@ body.simple-mode {
.simple-room-card .room-occupants {
display: flex;
flex-wrap: wrap;
gap: 6px;
gapvar(--space-150);
margin-bottom: var(--space-2);
}
@ -626,13 +626,13 @@ body.simple-mode {
.simple-quick-actions .action-badge {
position: absolute;
top: 4px;
right: 4px;
topvar(--space-1);
rightvar(--space-1);
background: var(--simple-accent-red);
color: var(--text-on-accent);
font-size: var(--text-3xs);
font-weight: 600;
padding: 2px 6px;
paddingvar(--space-half) var(--space-150);
border-radius: var(--radius-card);
min-width: 16px;
text-align: center;
@ -911,7 +911,7 @@ body.simple-mode {
.simple-security-toggle .security-toggle-btn {
width: 100%;
padding: 14px;
paddingvar(--space-350);
border-radius: var(--radius-card);
font-size: var(--text-base);
font-weight: 600;

View file

@ -429,7 +429,7 @@
display: flex;
align-items: flex-start;
gap: var(--space-2);
padding: 10px;
paddingvar(--space-250);
background: var(--bg-hover);
border-radius: var(--radius-control);
border-left: 3px solid var(--slate-7);
@ -459,7 +459,7 @@
.sim-shopping-list {
display: flex;
flex-direction: column;
gap: 6px;
gapvar(--space-150);
}
.sim-shopping-item {

View file

@ -4,8 +4,8 @@
.sleep-summary-card {
position: fixed;
top: 16px;
right: 16px;
topvar(--space-4);
rightvar(--space-4);
z-index: 1000;
background: var(--bg-card, var(--bg-card));
border: 1px solid var(--border-color, var(--slate-5));
@ -54,7 +54,7 @@
color: var(--text-muted, var(--text-muted));
font-size: var(--text-xl);
cursor: pointer;
padding: 2px 6px;
paddingvar(--space-half) var(--space-150);
line-height: 1;
border-radius: var(--radius-control);
}
@ -166,7 +166,7 @@
color: var(--text-muted, var(--text-muted));
font-size: var(--text-lg);
cursor: pointer;
padding: 2px 6px;
paddingvar(--space-half) var(--space-150);
border-radius: var(--radius-control);
}

View file

@ -51,7 +51,7 @@
background: transparent;
border: 1px solid var(--border-color, var(--bg-hover));
border-radius: var(--radius-control);
padding: 6px 10px;
paddingvar(--space-150) var(--space-250);
cursor: pointer;
color: var(--text-secondary, var(--text-secondary));
display: flex;
@ -115,8 +115,8 @@
.timeline-category-checkbox {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
gapvar(--space-150);
paddingvar(--space-150) var(--space-250);
background: var(--bg-primary, var(--bg-page));
border: 1px solid var(--border-color, var(--bg-hover));
border-radius: var(--radius-control);
@ -176,7 +176,7 @@
background: var(--bg-primary, var(--bg-page));
border: 1px solid var(--border-color, var(--bg-hover));
color: var(--text-primary, var(--slate-11));
padding: 6px 10px;
paddingvar(--space-150) var(--space-250);
border-radius: var(--radius-control);
font-size: var(--text-sm);
min-width: 100px;
@ -297,7 +297,7 @@
.timeline-event-header {
display: flex;
align-items: flex-start;
gap: 10px;
gapvar(--space-250);
margin-bottom: 6px;
}
@ -327,7 +327,7 @@
font-size: var(--text-sm);
font-weight: 500;
color: var(--text-primary, var(--slate-11));
margin: 2px;
marginvar(--space-half);
line-height: 1.3;
}
@ -629,7 +629,7 @@
color: var(--text-on-accent);
font-size: var(--text-2xs);
font-weight: 600;
padding: 2px 6px;
paddingvar(--space-half) var(--space-150);
border-radius: var(--radius-card);
margin-left: var(--space-2);
}
@ -654,7 +654,7 @@
.timeline-live-indicator {
display: flex;
align-items: center;
gap: 6px;
gapvar(--space-150);
font-size: var(--text-xs);
color: var(--color-success, var(--ok));
padding: var(--space-1) var(--space-2);
@ -706,8 +706,8 @@
#timeline-now-replaying {
display: none;
align-items: center;
gap: 6px;
padding: 2px 10px;
gapvar(--space-150);
paddingvar(--space-half) var(--space-250);
margin-left: var(--space-2);
background: color-mix(in srgb, var(--blue-9) 20%, transparent);
color: var(--blue-11);
@ -783,7 +783,7 @@
}
.timeline-event {
padding: 10px;
paddingvar(--space-250);
}
.timeline-event-actions {
@ -900,7 +900,7 @@
background: transparent;
border: none;
color: var(--text-secondary, var(--text-secondary));
padding: 6px;
paddingvar(--space-150);
border-radius: var(--radius-control);
cursor: pointer;
display: flex;
@ -943,7 +943,7 @@
padding: var(--space-2);
display: flex;
flex-direction: column;
gap: 6px;
gapvar(--space-150);
}
/* Compact Event Card for Sidebar */
@ -1040,7 +1040,7 @@
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
gapvar(--space-150);
font-size: var(--text-2xs);
color: var(--text-muted, var(--text-muted));
}
@ -1060,7 +1060,7 @@
/* Event Actions */
.sidebar-timeline-event-actions {
display: flex;
gap: 2px;
gapvar(--space-half);
flex-shrink: 0;
}
@ -1239,8 +1239,8 @@
.now-replaying-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 10px;
gapvar(--space-150);
paddingvar(--space-half) var(--space-250);
margin-left: var(--space-2);
background: color-mix(in srgb, var(--blue-9) 20%, transparent);
color: var(--blue-11);

View file

@ -14,7 +14,7 @@
background: var(--warn-bg);
border: 1px solid var(--warn-border);
border-radius: var(--radius-control);
padding: 10px;
paddingvar(--space-250);
margin-bottom: var(--space-2);
font-size: var(--text-xs);
animation: troubleshoot-fade-in 0.3s ease-out;
@ -28,7 +28,7 @@
.troubleshoot-card-header {
display: flex;
align-items: center;
gap: 6px;
gapvar(--space-150);
margin-bottom: var(--space-2);
color: var(--warn);
font-weight: 600;
@ -59,7 +59,7 @@
.troubleshoot-timeline {
display: flex;
flex-direction: column;
gap: 6px;
gapvar(--space-150);
}
.troubleshoot-step {
@ -285,7 +285,7 @@
/* Dismiss all button */
.spaxel-dismiss-all {
position: fixed;
bottom: 20px;
bottomvar(--space-5);
left: 50%;
transform: translateX(-50%);
background: var(--bg-hover);
@ -347,7 +347,7 @@
.post-cal-actions {
display: flex;
gap: 10px;
gapvar(--space-250);
justify-content: center;
flex-wrap: wrap;
}

444
dashboard/js/replay.test.js Normal file
View file

@ -0,0 +1,444 @@
/**
* Tests for SpaxelReplay time-travel module
*
* Covers:
* - jumpToTime creates replay session and fetches blobs
* - exitReplayMode cleans up state and notifies Viz3D
* - pauseLiveMode enters replay with correct time window
* - Timeline loop polls for session state during playback
* - Scrubber updates position based on session state
*/
'use strict';
// Mock DOM elements
function setupDOM() {
document.body.innerHTML = `
<div id="status-bar"></div>
<div id="scene-container"></div>
`;
}
// Mock Viz3D on both global and window
var viz3dMock = {
enterReplayMode: jest.fn(),
exitReplayMode: jest.fn(),
updateReplayBlobs: jest.fn()
};
global.Viz3D = viz3dMock;
// Mock SpaxelApp on both global and window
var appMock = {
showToast: jest.fn()
};
global.SpaxelApp = appMock;
// Mock SpaxelSidebarTimeline on both global and window
var sidebarMock = {
clearSelection: jest.fn(),
hideNowReplayingChip: jest.fn()
};
global.SpaxelSidebarTimeline = sidebarMock;
// Mock SpaxelRouter on both global and window
var routerMock = {
navigate: jest.fn()
};
global.SpaxelRouter = routerMock;
describe('SpaxelReplay', function() {
let Replay;
let fetchMock;
beforeEach(function() {
jest.clearAllMocks();
setupDOM();
// Default fetch mock returns store info
fetchMock = jest.fn(function(url, options) {
if (url === '/api/replay/sessions') {
return Promise.resolve({
ok: true,
json: function() {
return Promise.resolve({
has_data: true,
oldest_timestamp_ms: Date.now() - 3600000,
newest_timestamp_ms: Date.now(),
sessions: []
});
}
});
}
if (url === '/api/replay/jump-to-time') {
return Promise.resolve({
ok: true,
json: function() {
return Promise.resolve({
session_id: 'jump-session-1',
timestamp_ms: 1710519800000,
from_ms: 1710519795000,
to_ms: 1710519805000,
state: 'paused'
});
}
});
}
// Session state endpoint (with blobs)
var sessionMatch = url.match(/\/api\/replay\/session\/(.+)$/);
if (sessionMatch) {
return Promise.resolve({
ok: true,
json: function() {
return Promise.resolve({
session_id: sessionMatch[1],
current_ms: 1710519800000,
from_ms: 1710519795000,
to_ms: 1710519805000,
state: 'paused',
speed: 1,
blobs: [
{
id: 1,
x: 2.5,
y: 1.3,
z: 0.8,
vx: 0.1,
vy: 0.0,
vz: 0.0,
weight: 0.85,
posture: 'standing'
}
],
timestamp_ms: 1710519800000
});
}
});
}
// Stop session
if (url === '/api/replay/stop') {
return Promise.resolve({
ok: true,
json: function() {
return Promise.resolve({ status: 'stopped' });
}
});
}
// Start session
if (url === '/api/replay/start') {
return Promise.resolve({
ok: true,
json: function() {
return Promise.resolve({
session_id: 'start-session-1',
from_ms: 1710519740000,
to_ms: 1710519800000,
speed: 1,
state: 'paused'
});
}
});
}
return Promise.resolve({
ok: false,
statusText: 'Not Found'
});
});
global.fetch = fetchMock;
// Load the module
jest.isolateModules(function() {
require('./replay.js');
Replay = window.SpaxelReplay;
});
});
afterEach(function() {
document.body.innerHTML = '';
});
// ============================================
// Initialization Tests
// ============================================
describe('Initialization', function() {
test('initializes and fetches store info', function() {
expect(Replay).toBeTruthy();
expect(Replay.isReplayMode()).toBe(false);
expect(Replay.isPaused()).toBe(false);
// Should have called fetch for store info
expect(fetchMock).toHaveBeenCalledWith('/api/replay/sessions');
});
test('getSession returns initial empty state', function() {
var session = Replay.getSession();
expect(session.id).toBeNull();
expect(session.fromMs).toBeNull();
expect(session.state).toBe('stopped');
});
});
// ============================================
// Jump-to-Time Tests
// ============================================
describe('jumpToTime', function() {
test('creates replay session centered on timestamp', function() {
return Replay.jumpToTime(1710519800000).then(function(data) {
expect(fetchMock).toHaveBeenCalledWith('/api/replay/jump-to-time', expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({ 'Content-Type': 'application/json' })
}));
// Check the request body
var call = fetchMock.mock.calls.find(function(c) { return c[0] === '/api/replay/jump-to-time'; });
var body = JSON.parse(call[1].body);
expect(body.timestamp_ms).toBe(1710519800000);
expect(body.window_ms).toBe(5000);
});
});
test('uses custom window_ms when provided', function() {
fetchMock.mockImplementation(function(url) {
if (url === '/api/replay/jump-to-time') {
return Promise.resolve({
ok: true,
json: function() {
return Promise.resolve({
session_id: 'custom-window',
timestamp_ms: 1710519800000,
from_ms: 1710519790000,
to_ms: 1710519810000,
state: 'paused'
});
}
});
}
if (url.match(/\/api\/replay\/session\//)) {
return Promise.resolve({
ok: true,
json: function() {
return Promise.resolve({
session_id: 'custom-window',
current_ms: 1710519800000,
from_ms: 1710519790000,
to_ms: 1710519810000,
state: 'paused',
blobs: [],
timestamp_ms: 1710519800000
});
}
});
}
if (url === '/api/replay/sessions') {
return Promise.resolve({
ok: true,
json: function() { return Promise.resolve({ has_data: true, sessions: [] }); }
});
}
return Promise.resolve({ ok: false, statusText: 'Not Found' });
});
// Need to re-init with new fetch mock
jest.isolateModules(function() {
require('./replay.js');
var R = window.SpaxelReplay;
R.jumpToTime(1710519800000, 10000).then(function() {
var call = fetchMock.mock.calls.find(function(c) { return c[0] === '/api/replay/jump-to-time'; });
var body = JSON.parse(call[1].body);
expect(body.window_ms).toBe(10000);
});
});
return Promise.resolve();
});
test('enters replay mode after successful jump', function() {
return Replay.jumpToTime(1710519800000).then(function() {
expect(Replay.isReplayMode()).toBe(true);
});
});
test('shows replay control bar after jump', function() {
return Replay.jumpToTime(1710519800000).then(function() {
var bar = document.getElementById('replay-control-bar');
expect(bar).toBeTruthy();
expect(bar.style.display).toBe('block');
});
});
test('calls Viz3D.enterReplayMode', function() {
return Replay.jumpToTime(1710519800000).then(function() {
expect(global.Viz3D.enterReplayMode).toHaveBeenCalled();
});
});
test('fetches session blobs and feeds them to Viz3D', function() {
return Replay.jumpToTime(1710519800000).then(function() {
// Should have fetched session state for blobs
var sessionCall = fetchMock.mock.calls.find(function(c) {
return c[0] && c[0].match(/\/api\/replay\/session\//);
});
expect(sessionCall).toBeTruthy();
// Should have fed blobs to Viz3D
expect(global.Viz3D.updateReplayBlobs).toHaveBeenCalledWith(
expect.arrayContaining([
expect.objectContaining({
id: 1,
x: 2.5,
y: 1.3,
z: 0.8
})
]),
1710519800000
);
});
});
test('updates session state from jump response', function() {
return Replay.jumpToTime(1710519800000).then(function() {
var session = Replay.getSession();
expect(session.id).toBe('jump-session-1');
expect(session.currentMs).toBe(1710519800000);
expect(session.state).toBe('paused');
});
});
test('handles API error gracefully', function() {
fetchMock.mockImplementation(function(url) {
if (url === '/api/replay/sessions') {
return Promise.resolve({
ok: true,
json: function() { return Promise.resolve({ has_data: false, sessions: [] }); }
});
}
if (url === '/api/replay/jump-to-time') {
return Promise.resolve({
ok: false,
statusText: 'Internal Server Error'
});
}
return Promise.resolve({ ok: false, statusText: 'Not Found' });
});
jest.isolateModules(function() {
require('./replay.js');
return window.SpaxelReplay.jumpToTime(1710519800000).catch(function(err) {
expect(err.message).toContain('Failed to jump to time');
});
});
});
});
// ============================================
// Exit Replay Mode Tests
// ============================================
describe('exitReplayMode', function() {
test('exits replay mode and clears state', function() {
return Replay.jumpToTime(1710519800000).then(function() {
expect(Replay.isReplayMode()).toBe(true);
return Replay.exitReplay();
}).then(function() {
expect(Replay.isReplayMode()).toBe(false);
expect(Replay.isPaused()).toBe(false);
});
});
test('hides replay control bar', function() {
return Replay.jumpToTime(1710519800000).then(function() {
return Replay.exitReplay();
}).then(function() {
var bar = document.getElementById('replay-control-bar');
expect(bar.style.display).toBe('none');
});
});
test('calls Viz3D.exitReplayMode', function() {
return Replay.jumpToTime(1710519800000).then(function() {
return Replay.exitReplay();
}).then(function() {
expect(global.Viz3D.exitReplayMode).toHaveBeenCalled();
});
});
test('clears sidebar timeline selection and chip', function() {
return Replay.jumpToTime(1710519800000).then(function() {
return Replay.exitReplay();
}).then(function() {
expect(global.SpaxelSidebarTimeline.clearSelection).toHaveBeenCalled();
expect(global.SpaxelSidebarTimeline.hideNowReplayingChip).toHaveBeenCalled();
});
});
test('navigates back to live mode', function() {
return Replay.jumpToTime(1710519800000).then(function() {
return Replay.exitReplay();
}).then(function() {
expect(global.SpaxelRouter.navigate).toHaveBeenCalledWith('live');
});
});
test('stops the replay session via API', function() {
return Replay.jumpToTime(1710519800000).then(function() {
return Replay.exitReplay();
}).then(function() {
var stopCall = fetchMock.mock.calls.find(function(c) { return c[0] === '/api/replay/stop'; });
expect(stopCall).toBeTruthy();
var body = JSON.parse(stopCall[1].body);
expect(body.session_id).toBe('jump-session-1');
});
});
});
// ============================================
// Pause Live Mode Tests
// ============================================
describe('pauseLiveMode', function() {
test('creates replay window 60 seconds before now', function() {
var beforeMs = Date.now();
Replay.pauseLive();
var startCall = fetchMock.mock.calls.find(function(c) { return c[0] === '/api/replay/start'; });
expect(startCall).toBeTruthy();
var body = JSON.parse(startCall[1].body);
var fromMs = new Date(body.from_iso8601).getTime();
var toMs = new Date(body.to_iso8601).getTime();
// Should be approximately 60s window
var windowMs = toMs - fromMs;
expect(windowMs).toBeGreaterThanOrEqual(59000);
expect(windowMs).toBeLessThanOrEqual(61000);
expect(fromMs).toBeGreaterThanOrEqual(beforeMs - 61000);
});
});
// ============================================
// Replay Control Bar UI Tests
// ============================================
describe('Replay Control Bar', function() {
test('control bar is created in the DOM', function() {
var bar = document.getElementById('replay-control-bar');
expect(bar).toBeTruthy();
});
test('control bar has all required buttons', function() {
expect(document.getElementById('replay-back-btn')).toBeTruthy();
expect(document.getElementById('replay-play-btn')).toBeTruthy();
expect(document.getElementById('replay-speed')).toBeTruthy();
expect(document.getElementById('replay-scrubber')).toBeTruthy();
expect(document.getElementById('replay-close-btn')).toBeTruthy();
expect(document.getElementById('replay-tune-btn')).toBeTruthy();
});
test('control bar is hidden by default', function() {
var bar = document.getElementById('replay-control-bar');
expect(bar.style.display).toBe('none');
});
});
});