feat: implement expert vs simple mode for timeline panel

- Add mode switching for timeline panel with ?mode=expert or ?mode=simple
- Expert mode displays all event types with system events as secondary (smaller, greyed)
- Simple mode shows only person-relevant events: ZoneTransition, FallDetected, AnomalyDetected, SleepSessionEnd, zone_entry/exit, portal_crossing, fall_alert, anomaly, security_alert
- Backend defaults to expert mode when mode parameter is empty or invalid
- Frontend syncs dashboard mode with SpaxelSimpleModeDetection for mode changes
- Add CSS styling for new event types (ZoneTransition, FallDetected, AnomalyDetected, sleep_session_end)
- Update isValidEventType to include new event types
This commit is contained in:
jedarden 2026-04-11 13:21:28 -04:00
parent c9b36092d5
commit 6bfe4aad01
3 changed files with 483 additions and 16 deletions

View file

@ -466,6 +466,11 @@
--event-color-bg: rgba(255, 107, 107, 0.15);
}
.timeline-event[data-type="ZoneTransition"] {
--event-color: #ffa726;
--event-color-bg: rgba(255, 167, 38, 0.15);
}
.timeline-event[data-type="portal_crossing"] {
--event-color: #4a9eff;
--event-color-bg: rgba(74, 158, 255, 0.15);
@ -476,11 +481,21 @@
--event-color-bg: rgba(255, 71, 87, 0.15);
}
.timeline-event[data-type="AnomalyDetected"] {
--event-color: #ff4757;
--event-color-bg: rgba(255, 71, 87, 0.15);
}
.timeline-event[data-type="anomaly_detected"] {
--event-color: #ff4757;
--event-color-bg: rgba(255, 71, 87, 0.15);
}
.timeline-event[data-type="FallDetected"] {
--event-color: #f44336;
--event-color-bg: rgba(244, 67, 54, 0.15);
}
.timeline-event[data-type="security_alert"] {
--event-color: #ffa502;
--event-color-bg: rgba(255, 165, 2, 0.15);
@ -496,6 +511,11 @@
--event-color-bg: rgba(116, 125, 140, 0.15);
}
.timeline-event[data-type="sleep_session_end"] {
--event-color: #4fc3f7;
--event-color-bg: rgba(79, 195, 247, 0.15);
}
/* Loading States */
.timeline-loading {
display: flex;
@ -757,3 +777,395 @@
.timeline-date-apply-btn:hover {
background: #3a8ee6;
}
/* ============================================
Sidebar Timeline Panel Styles
============================================ */
/* Main Sidebar Panel Container */
.sidebar-panel {
position: fixed;
top: 44px; /* Below status bar */
right: 0;
width: 320px;
height: calc(100vh - 44px);
background: var(--bg-secondary, #16162a);
border-left: 1px solid var(--border-color, #2a2a4a);
display: flex;
flex-direction: column;
z-index: 60;
transition: transform 0.3s ease, opacity 0.3s ease;
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
}
.sidebar-panel.collapsed {
transform: translateX(100%);
opacity: 0;
pointer-events: none;
}
/* Panel Header */
.sidebar-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--bg-tertiary, #1a1a2e);
border-bottom: 1px solid var(--border-color, #2a2a4a);
flex-shrink: 0;
}
.sidebar-panel-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--text-primary, #e0e0e0);
}
.sidebar-panel-title svg {
color: var(--accent-color, #4a9eff);
}
.sidebar-panel-actions {
display: flex;
gap: 4px;
}
.sidebar-panel-btn {
background: transparent;
border: none;
color: var(--text-secondary, #a0a0b0);
padding: 6px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.sidebar-panel-btn:hover {
background: var(--bg-primary, #121225);
color: var(--text-primary, #e0e0e0);
}
/* Panel Content */
.sidebar-panel-content {
flex: 1;
overflow-y: auto;
position: relative;
}
.sidebar-panel-content::-webkit-scrollbar {
width: 4px;
}
.sidebar-panel-content::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-panel-content::-webkit-scrollbar-thumb {
background: var(--border-color, #2a2a4a);
border-radius: 2px;
}
.sidebar-panel-content::-webkit-scrollbar-thumb:hover {
background: var(--text-muted, #666);
}
/* Timeline Events in Sidebar */
.sidebar-timeline-events {
padding: 8px;
display: flex;
flex-direction: column;
gap: 6px;
}
/* Compact Event Card for Sidebar */
.sidebar-timeline-event {
background: var(--bg-primary, #121225);
border: 1px solid var(--border-color, #2a2a4a);
border-radius: 6px;
padding: 8px 10px;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
display: flex;
gap: 8px;
align-items: flex-start;
}
.sidebar-timeline-event::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 3px;
background: var(--event-color, #4a9eff);
}
.sidebar-timeline-event:hover {
border-color: var(--accent-color, #4a9eff);
transform: translateX(-2px);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
}
.sidebar-timeline-event.severity-critical {
border-color: var(--color-critical, #ff4757);
}
.sidebar-timeline-event.severity-critical::before {
background: var(--color-critical, #ff4757);
}
.sidebar-timeline-event.severity-warning {
border-color: var(--color-warning, #ffa502);
}
.sidebar-timeline-event.severity-warning::before {
background: var(--color-warning, #ffa502);
}
.sidebar-timeline-event.new-event {
animation: sidebarEventSlideIn 0.3s ease-out;
}
@keyframes sidebarEventSlideIn {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Event Icon */
.sidebar-timeline-event-icon {
flex-shrink: 0;
font-size: 16px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(74, 158, 255, 0.1);
border-radius: 4px;
}
/* Event Content */
.sidebar-timeline-event-content {
flex: 1;
min-width: 0;
}
.sidebar-timeline-event-title {
font-size: 13px;
font-weight: 500;
color: var(--text-primary, #e0e0e0);
line-height: 1.3;
margin-bottom: 2px;
word-wrap: break-word;
}
.sidebar-timeline-event-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--text-muted, #888);
}
.sidebar-timeline-event-time {
color: var(--text-secondary, #a0a0b0);
}
.sidebar-timeline-event-zone {
color: var(--accent-color, #4a9eff);
}
.sidebar-timeline-event-person {
color: #81c784;
}
/* Event Actions */
.sidebar-timeline-event-actions {
display: flex;
gap: 2px;
flex-shrink: 0;
}
.sidebar-timeline-action-btn {
background: transparent;
border: none;
color: var(--text-muted, #888);
padding: 4px;
border-radius: 3px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
font-size: 12px;
min-width: 24px;
height: 24px;
}
.sidebar-timeline-action-btn:hover {
background: var(--bg-tertiary, #1a1a2e);
color: var(--text-secondary, #a0a0b0);
}
.sidebar-timeline-action-btn.feedback-positive:hover {
background: rgba(76, 175, 80, 0.2);
color: #81c784;
}
.sidebar-timeline-action-btn.feedback-negative:hover {
background: rgba(244, 67, 54, 0.2);
color: #e57373;
}
.sidebar-timeline-action-btn.active {
background: var(--accent-color, #4a9eff);
color: var(--bg-primary, #121225);
}
/* Feedback Dismissed State */
.sidebar-timeline-event.feedback-dismissed {
opacity: 0.5;
text-decoration: line-through;
}
/* Loading State */
.sidebar-timeline-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
color: var(--text-muted, #888);
gap: 12px;
}
.sidebar-spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border-color, #2a2a4a);
border-top-color: var(--accent-color, #4a9eff);
border-radius: 50%;
animation: sidebarSpin 0.8s linear infinite;
}
@keyframes sidebarSpin {
to { transform: rotate(360deg); }
}
/* Empty State */
.sidebar-timeline-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
color: var(--text-muted, #888);
}
.sidebar-timeline-empty svg {
color: var(--border-color, #2a2a4a);
margin-bottom: 12px;
}
.sidebar-timeline-empty h3 {
font-size: 14px;
color: var(--text-secondary, #a0a0b0);
margin: 0 0 4px 0;
}
.sidebar-timeline-empty p {
font-size: 12px;
margin: 0;
}
/* Show Button (when panel is collapsed) */
.sidebar-show-btn {
position: fixed;
top: 50%;
right: 0;
transform: translateY(-50%);
background: var(--bg-secondary, #16162a);
border: 1px solid var(--border-color, #2a2a4a);
border-right: none;
border-radius: 8px 0 0 8px;
padding: 12px 8px;
cursor: pointer;
z-index: 59;
transition: all 0.3s ease;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.2);
}
.sidebar-show-btn:hover {
background: var(--bg-tertiary, #1a1a2e);
padding-right: 12px;
}
.sidebar-show-btn svg {
color: var(--text-secondary, #a0a0b0);
}
.sidebar-show-btn.hidden {
transform: translateY(-50%) translateX(100%);
opacity: 0;
pointer-events: none;
}
/* Virtual Spacers */
.timeline-spacer {
flex-shrink: 0;
}
/* Event Type Colors */
.sidebar-timeline-event[data-type="zone_entry"] { --event-color: #66bb6a; }
.sidebar-timeline-event[data-type="zone_exit"] { --event-color: #ffa726; }
.sidebar-timeline-event[data-type="portal_crossing"] { --event-color: #42a5f5; }
.sidebar-timeline-event[data-type="detection"] { --event-color: #ab47bc; }
.sidebar-timeline-event[data-type="presence_transition"] { --event-color: #ab47bc; }
.sidebar-timeline-event[data-type="stationary_detected"] { --event-color: #7e57c2; }
.sidebar-timeline-event[data-type="anomaly"] { --event-color: #ef5350; }
.sidebar-timeline-event[data-type="security_alert"] { --event-color: #d32f2f; }
.sidebar-timeline-event[data-type="fall_alert"] { --event-color: #f44336; }
.sidebar-timeline-event[data-type="node_online"] { --event-color: #4caf50; }
.sidebar-timeline-event[data-type="node_offline"] { --event-color: #9e9e9e; }
.sidebar-timeline-event[data-type="ota_update"] { --event-color: #2196f3; }
.sidebar-timeline-event[data-type="baseline_changed"] { --event-color: #00bcd4; }
.sidebar-timeline-event[data-type="system"] { --event-color: #607d8b; }
.sidebar-timeline-event[data-type="learning_milestone"] { --event-color: #9c27b0; }
.sidebar-timeline-event[data-type="anomaly_learned"] { --event-color: #9c27b0; }
/* Responsive Design */
@media (max-width: 768px) {
.sidebar-panel {
width: 100%;
max-width: 320px;
}
.sidebar-show-btn {
display: none; /* Always show panel on mobile, use close button instead */
}
}
/* Secondary System Events (dimmed in expert mode) */
.sidebar-timeline-event.secondary {
opacity: 0.6;
}
.sidebar-timeline-event.secondary:hover {
opacity: 0.9;
}

View file

@ -32,8 +32,8 @@
// ============================================
const EVENT_CATEGORIES = {
presence: ['presence_transition', 'stationary_detected', 'detection'],
zones: ['zone_entry', 'zone_exit', 'portal_crossing'],
alerts: ['fall_alert', 'anomaly', 'security_alert'],
zones: ['zone_entry', 'zone_exit', 'ZoneTransition', 'portal_crossing', 'sleep_session_end'],
alerts: ['fall_alert', 'FallDetected', 'anomaly', 'AnomalyDetected', 'security_alert'],
system: ['node_online', 'node_offline', 'ota_update', 'baseline_changed', 'system'],
learning: ['learning_milestone', 'anomaly_learned']
};
@ -105,6 +105,13 @@
description: 'Person exited a zone',
category: 'zones'
},
ZoneTransition: {
icon: '🚶',
color: '#ffa726',
label: 'Moved',
description: 'Person moved between zones',
category: 'zones'
},
portal_crossing: {
icon: '→',
color: '#42a5f5',
@ -112,6 +119,13 @@
description: 'Person crossed a portal',
category: 'zones'
},
sleep_session_end: {
icon: '🌅',
color: '#4fc3f7',
label: 'Woke Up',
description: 'Sleep session ended',
category: 'zones'
},
presence_transition: {
icon: '👤',
color: '#ab47bc',
@ -133,6 +147,20 @@
description: 'Motion detected',
category: 'presence'
},
fall_alert: {
icon: '🆘',
color: '#f44336',
label: 'Fall',
description: 'Fall detected',
category: 'alerts'
},
FallDetected: {
icon: '🆘',
color: '#f44336',
label: 'Fall',
description: 'Fall detected',
category: 'alerts'
},
anomaly: {
icon: '⚠️',
color: '#ef5350',
@ -140,6 +168,13 @@
description: 'Unusual activity detected',
category: 'alerts'
},
AnomalyDetected: {
icon: '⚠️',
color: '#ef5350',
label: 'Anomaly',
description: 'Unusual activity detected',
category: 'alerts'
},
security_alert: {
icon: '🚨',
color: '#d32f2f',
@ -147,13 +182,6 @@
description: 'Security alert',
category: 'alerts'
},
fall_alert: {
icon: '🆘',
color: '#f44336',
label: 'Fall',
description: 'Fall detected',
category: 'alerts'
},
node_online: {
icon: '📡',
color: '#4caf50',
@ -220,12 +248,36 @@
SpaxelRouter.onModeChange(onModeChange);
}
// Listen for simple mode changes
if (window.SpaxelSimpleModeDetection) {
SpaxelSimpleModeDetection.onModeChange(onSimpleModeChange);
}
// Listen for WebSocket event messages
if (window.SpaxelApp) {
SpaxelApp.registerMessageHandler(handleWebSocketMessage);
}
}
// ============================================
// Simple Mode Change Handler
// ============================================
function onSimpleModeChange(newMode, oldMode) {
console.log('[Timeline] Simple mode changed from', oldMode, 'to', newMode);
// Update dashboard mode based on simple mode
if (newMode === 'simple') {
state.dashboardMode = 'simple';
} else {
state.dashboardMode = 'expert';
}
// Reload events if timeline is visible
if (elements.container && elements.container.style.display !== 'none') {
loadInitialEvents();
}
}
// ============================================
// Virtualization Setup
// ============================================
@ -683,8 +735,8 @@
elements.categoryPresence,
elements.categoryZones,
elements.categoryAlerts,
elements.categorySystem,
elements.categoryLearning
elements.categorySystem,
elements.categoryLearning
];
checkboxes.forEach(function(cb) {
if (cb) {

View file

@ -180,9 +180,9 @@ func createEventsSchema(db *sql.DB) error {
func isValidEventType(t string) bool {
switch t {
case "detection", "zone_entry", "zone_exit", "portal_crossing",
"trigger_fired", "fall_alert", "anomaly", "anomaly_detected", "security_alert",
"trigger_fired", "fall_alert", "FallDetected", "anomaly", "AnomalyDetected", "security_alert",
"node_online", "node_offline", "ota_update", "baseline_changed",
"system", "learning_milestone", "sleep_session_end":
"system", "learning_milestone", "sleep_session_end", "ZoneTransition":
return true
}
return false
@ -316,19 +316,22 @@ func (e *EventsHandler) listEvents(w http.ResponseWriter, r *http.Request) {
}
// Person-relevant event types for simple mode
// Simple mode shows only person-relevant events: zone_entry, zone_exit, portal_crossing, fall_alert, anomaly, anomaly_detected, security_alert, sleep_session_end
// Simple mode shows only person-relevant events: zone_entry, zone_exit, ZoneTransition, portal_crossing, fall_alert, FallDetected, anomaly, AnomalyDetected, security_alert, sleep_session_end
// Simple mode hides: node_online, node_offline, ota_update, baseline_changed, system, learning_milestone, detection, presence_transition, stationary_detected
simpleModeTypes := map[string]bool{
"zone_entry": true,
"zone_exit": true,
"ZoneTransition": true,
"portal_crossing": true,
"fall_alert": true,
"FallDetected": true,
"anomaly": true,
"anomaly_detected": true,
"AnomalyDetected": true,
"security_alert": true,
"sleep_session_end": true,
}
isSimpleMode := mode != "expert"
// Default to expert mode when mode parameter is empty or invalid
isSimpleMode := mode == "simple"
// Prepare FTS5 query with prefix matching
if q != "" {