`;
}
/**
* Render loading state
*/
function renderLoadingState() {
return `
Loading your home...
`;
}
// ============================================
// Event Handlers
// ============================================
/**
* Attach event listeners to rendered elements
*/
function attachEventListeners() {
// Alert dismiss buttons
document.querySelectorAll('.alert-dismiss').forEach(btn => {
btn.addEventListener('click', dismissAlert);
});
// Briefing dismiss button
document.querySelector('.briefing-dismiss')?.addEventListener('click', dismissBriefing);
// Security toggle buttons
document.querySelectorAll('[data-action="arm-security"], [data-action="disarm-security"]')
.forEach(btn => btn.addEventListener('click', toggleSecurityMode));
// Room card clicks
document.querySelectorAll('.simple-room-card').forEach(card => {
card.addEventListener('click', () => showRoomDetails(card.dataset.zoneId));
});
// Activity filter buttons
document.querySelectorAll('.feed-filter-btn').forEach(btn => {
btn.addEventListener('click', filterActivityFeed);
});
}
/**
* Handle quick action button clicks
*/
function onQuickAction(e) {
const action = e.currentTarget.dataset.action;
switch (action) {
case 'home':
// Scroll to top
window.scrollTo({ top: 0, behavior: 'smooth' });
break;
case 'timeline':
// Switch to timeline view
disableSimpleMode();
if (window.SpaxelRouter) {
SpaxelRouter.navigate('timeline');
}
break;
case 'security':
// Scroll to security toggle or toggle it
const securityToggle = document.querySelector('.simple-security-toggle');
if (securityToggle) {
securityToggle.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
break;
case 'settings':
// Switch to expert mode and open settings
disableSimpleMode();
if (window.openSettingsPanel) {
openSettingsPanel();
}
break;
}
// Update active state
document.querySelectorAll('.quick-action-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.action === action);
});
}
/**
* Dismiss an alert
*/
function dismissAlert(e) {
const banner = e.target.closest('.simple-alert-banner');
if (banner) {
banner.style.animation = 'slideDown 0.3s ease-out reverse';
setTimeout(() => banner.remove(), 300);
}
}
/**
* Dismiss the morning briefing
*/
function dismissBriefing() {
const today = new Date().toISOString().split('T')[0];
localStorage.setItem(STORAGE_KEY_DISMISSED, today);
const briefing = document.querySelector('.simple-morning-briefing');
if (briefing) {
briefing.style.animation = 'fadeIn 0.3s ease-out reverse';
setTimeout(() => briefing.remove(), 300);
}
}
/**
* Toggle security mode
*/
async function toggleSecurityMode(e) {
const isArming = e.target.dataset.action === 'arm-security';
const endpoint = isArming ? '/api/security/arm' : '/api/security/disarm';
try {
const response = await fetch(endpoint, { method: 'POST' });
if (response.ok) {
// Update state and re-render
currentState.securityMode = isArming;
renderContent();
// Show toast confirmation
showToast(isArming ? 'Security mode armed' : 'Security mode disarmed');
} else {
showError('Failed to toggle security mode');
}
} catch (error) {
console.error('[Simple Mode] Error toggling security:', error);
showError('Unable to toggle security mode');
}
}
/**
* Show room details modal
*/
function showRoomDetails(zoneId) {
const zone = currentState.zones.find(z => z.id == zoneId);
if (!zone) return;
// Create modal
const modal = document.createElement('div');
modal.className = 'simple-room-modal visible';
modal.innerHTML = `
${zone.name}
Occupancy
${zone.occupancy || 0}
People
${(zone.people || []).length}
Recent Activity
${getZoneHistory(zone.name)}
`;
document.body.appendChild(modal);
// Close on backdrop click or close button
modal.addEventListener('click', (e) => {
if (e.target === modal || e.target.classList.contains('modal-close')) {
modal.remove();
}
});
}
/**
* Filter activity feed
*/
function filterActivityFeed(e) {
const filter = e.target.dataset.filter;
// Update active state
document.querySelectorAll('.feed-filter-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.filter === filter);
});
// Re-render with filter applied
// (In a full implementation, this would filter the events array)
console.log('[Simple Mode] Filter activity feed:', filter);
}
// ============================================
// Helper Functions
// ============================================
/**
* Get greeting based on time of day
*/
function getGreeting() {
const hour = new Date().getHours();
if (hour < 12) return 'Good morning';
if (hour < 17) return 'Good afternoon';
return 'Good evening';
}
/**
* Format date for display
*/
function formatDate(timestamp) {
const date = new Date(timestamp);
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
if (date.toDateString() === today.toDateString()) {
return 'Today';
} else if (date.toDateString() === yesterday.toDateString()) {
return 'Yesterday';
} else {
return date.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' });
}
}
/**
* Format timestamp for display
*/
function formatTimestamp(ms) {
const date = new Date(ms);
const now = new Date();
const diff = now - date;
// Less than 1 minute
if (diff < 60000) {
return 'Just now';
}
// Less than 1 hour
if (diff < 3600000) {
const mins = Math.floor(diff / 60000);
return `${mins}m ago`;
}
// Less than 1 day
if (diff < 86400000) {
const hours = Math.floor(diff / 3600000);
return `${hours}h ago`;
}
// Otherwise show date
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}
/**
* Format duration in minutes to hours and minutes
*/
function formatDuration(minutes) {
if (!minutes) return '--';
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (hours > 0) {
return `${hours}h ${mins}m`;
}
return `${mins}m`;
}
/**
* Parse briefing content into sections
*/
function parseBriefingContent(content) {
// Simple parsing - in production, this would be more sophisticated
const lines = content.split('\n').filter(line => line.trim());
return lines.map(line => `
${line}
`).join('');
}
/**
* Get zone status
*/
function getZoneStatus(zone) {
const count = zone.Count || 0;
if (count > 0) {
return { class: 'occupied', label: `Occupied (${count})` };
}
return { class: 'empty', label: 'Empty' };
}
/**
* Get zone color (consistent color per zone name)
*/
function getZoneColor(zoneName) {
// Generate consistent color from zone name
let hash = 0;
for (let i = 0; i < zoneName.length; i++) {
hash = zoneName.charCodeAt(i) + ((hash << 5) - hash);
}
const hue = Math.abs(hash) % 360;
return `hsl(${hue}, 70%, 50%)`;
}
/**
* Get person color
*/
function getPersonColor(person) {
// Generate consistent color from name
let hash = 0;
for (let i = 0; i < person.length; i++) {
hash = person.charCodeAt(i) + ((hash << 5) - hash);
}
const hue = Math.abs(hash) % 360;
return `hsl(${hue}, 70%, 50%)`;
}
/**
* Get person initials
*/
function getPersonInitials(person) {
const parts = person.trim().split(/\s+/);
if (parts.length >= 2) {
return (parts[0][0] + parts[1][0]).toUpperCase();
}
return person.substring(0, 2).toUpperCase();
}
/**
* Get last activity for a zone
*/
function getLastActivityForZone(zoneName) {
const zoneEvents = currentState.events.filter(e => e.zone === zoneName);
if (zoneEvents.length > 0) {
const latest = zoneEvents[0];
return formatEventDescription(latest);
}
return 'No recent activity';
}
/**
* Get activity icon for event type
*/
function getActivityIcon(type) {
const icons = {
'detection': { icon: '👤', class: 'presence' },
'zone_entry': { icon: '🚪', class: 'presence' },
'zone_exit': { icon: '🚪', class: 'presence' },
'portal_crossing': { icon: '🚪', class: 'presence' },
'fall_alert': { icon: '🩸', class: 'alert' },
'anomaly': { icon: '⚠', class: 'alert' },
'security_alert': { icon: '🔒', class: 'alert' },
'node_online': { icon: '📱', class: 'system' },
'node_offline': { icon: '📱', class: 'system' },
'system': { icon: '⚙', class: 'system' },
'learning_milestone': { icon: '📅', class: 'system' }
};
return icons[type] || { icon: '•', class: 'presence' };
}
/**
* Format event title
*/
function formatEventTitle(event) {
if (event.title) return event.title;
const titles = {
'detection': 'Motion detected',
'zone_entry': `Entered ${event.zone}`,
'zone_exit': `Left ${event.zone}`,
'portal_crossing': 'Room transition',
'fall_alert': 'Fall detected',
'anomaly': 'Unusual activity',
'security_alert': 'Security alert',
'node_online': 'Node connected',
'node_offline': 'Node disconnected',
'system': 'System event',
'learning_milestone': 'Learning progress'
};
return titles[event.type] || 'Event';
}
/**
* Format event description
*/
function formatEventDescription(event) {
if (event.detail_json) {
try {
const detail = typeof event.detail_json === 'string'
? JSON.parse(event.detail_json)
: event.detail_json;
return detail.description || detail.message || '';
} catch (e) {
// Ignore parse errors
}
}
// Default descriptions
const descriptions = {
'detection': 'Motion was detected in this area',
'zone_entry': `Someone entered ${event.zone}`,
'zone_exit': `Someone left ${event.zone}`,
'portal_crossing': 'Movement between rooms detected',
'fall_alert': 'A possible fall was detected',
'anomaly': 'Activity outside normal patterns',
'security_alert': 'Security mode was triggered',
'node_online': 'A node came online',
'node_offline': 'A node went offline',
'system': 'System status changed',
'learning_milestone': 'System learned something new'
};
return descriptions[event.type] || '';
}
/**
* Get zone history HTML
*/
function getZoneHistory(zoneName) {
const zoneEvents = currentState.events
.filter(e => e.zone === zoneName)
.slice(0, 5);
if (zoneEvents.length === 0) {
return '