TILES Engineering Systems Architecture

Document Version 2.0.0 — Modular ES6+ Architecture — November 2025

Table of Contents

1. System Overview

TILES is a web-based smart home control interface designed for Hubitat Elevation home automation hubs. The application provides real-time device control and monitoring through a responsive, mobile-first dashboard interface. The system supports multiple device types including lights, switches, dimmers, thermostats, locks, and power meters.

The architecture follows a modular ES6+ design pattern, featuring a complete separation of concerns across specialized modules for API communication, state management, device abstraction, and UI rendering. Real-time updates are achieved through WebSocket connections to the Hubitat hub's event socket.

Core Architecture Principles: The system implements a factory pattern for device instantiation, a publish-subscribe pattern for state management, and an inheritance-based device class hierarchy. Each device type extends a common BaseDevice class, ensuring consistent behavior while allowing specialized functionality per device category.

Primary Capabilities

2. Technology Stack

ES6+ JavaScript jQuery 3.x Bootstrap 5 axios roundSlider Flask Gunicorn Docker Bootstrap Icons
Layer Technology Purpose
Frontend Core ES6+ JavaScript Modules Modular application architecture with native import/export
DOM Manipulation jQuery 3.x DOM operations, event handling, AJAX utilities
HTTP Client axios REST API communication with request cancellation support
UI Framework Bootstrap 5 Responsive grid, modals, components
Specialized UI roundSlider Circular thermostat temperature controls
Icons Bootstrap Icons Device state iconography (locks, lights, etc.)
Web Server Flask + Gunicorn Static file serving in production
Containerization Docker Consistent deployment environment
Real-Time Native WebSocket Hubitat event socket connection

3. Architecture Overview

Layered Architecture: The application follows a clear separation between presentation, business logic, and data access layers. The modular design enables independent testing, maintenance, and extension of individual components without affecting the broader system.
graph TB subgraph CLIENT["Client Browser"] subgraph APP["TilesApp (Orchestrator)"] INIT["Application Bootstrap"] end subgraph UI_LAYER["UI Layer"] UIM["UIManager"] PM["PanelManager"] OM["OverlayManager"] end subgraph DEVICE_LAYER["Device Layer"] DF["DeviceFactory"] BD["BaseDevice"] LD["LightDevice"] DD["DimmerDevice"] SD["SwitchDevice"] LKD["LockDevice"] TD["ThermostatDevice"] PMD["PowerMeterDevice"] end subgraph STATE_LAYER["State Layer"] DSM["DeviceStateManager"] SM["SettingsManager"] end subgraph COMM_LAYER["Communication Layer"] API["HubitatAPI"] RQ["RequestQueue"] WSM["WebSocketManager"] end end subgraph HUBITAT["Hubitat Elevation Hub"] MAKER["Maker API"] EVTSOCK["Event Socket"] DEVICES["Physical Devices"] end %% Initialization Flow INIT --> SM INIT --> API INIT --> DSM INIT --> DF INIT --> UIM INIT --> WSM %% Device Creation DF --> BD BD --> LD BD --> DD BD --> SD BD --> LKD BD --> TD BD --> PMD %% UI Rendering UIM --> PM UIM --> OM DF --> UIM %% State Management DSM --> BD WSM --> DSM %% API Communication API --> RQ API --> MAKER WSM --> EVTSOCK %% Hubitat Internal MAKER --> DEVICES DEVICES --> EVTSOCK %% Styling classDef orchestrator fill:#e1f5fe,stroke:#0277bd,stroke-width:2px classDef ui fill:#fff3e0,stroke:#f57c00,stroke-width:2px classDef device fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px classDef state fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px classDef comm fill:#fce4ec,stroke:#c2185b,stroke-width:2px classDef hubitat fill:#e0e0e0,stroke:#424242,stroke-width:2px class INIT orchestrator class UIM,PM,OM ui class DF,BD,LD,DD,SD,LKD,TD,PMD device class DSM,SM state class API,RQ,WSM comm class MAKER,EVTSOCK,DEVICES hubitat

4. Module Architecture

The application is structured as a collection of ES6 modules, each with a single responsibility. The modular architecture enables clean dependency management through native JavaScript imports and provides clear boundaries between system components.

graph LR subgraph ENTRY["Entry Point"] TILES["tiles-modular.js"] end subgraph CORE["Core Module"] TA["TilesApp.js"] end subgraph API_MOD["API Modules"] HAPI["HubitatAPI.js"] RQ["RequestQueue.js"] end subgraph CONFIG["Config Modules"] SETTINGS["SettingsManager.js"] end subgraph DEVICES_MOD["Device Modules"] FACTORY["DeviceFactory.js"] BASE["BaseDevice.js"] LIGHT["LightDevice.js"] DIMMER["DimmerDevice.js"] SWITCH["SwitchDevice.js"] LOCK["LockDevice.js"] THERMO["ThermostatDevice.js"] POWER["PowerMeterDevice.js"] end subgraph STATE_MOD["State Modules"] DSM["DeviceStateManager.js"] end subgraph UI_MOD["UI Modules"] UIM["UIManager.js"] PANEL["PanelManager.js"] OVERLAY["OverlayManager.js"] end subgraph WS_MOD["WebSocket Modules"] WSM["WebSocketManager.js"] end subgraph UTILS["Utility Modules"] CONST["constants.js"] HELP["helpers.js"] end TILES --> TA TA --> HAPI TA --> SETTINGS TA --> FACTORY TA --> DSM TA --> UIM TA --> WSM HAPI --> RQ FACTORY --> BASE BASE --> LIGHT BASE --> DIMMER BASE --> SWITCH BASE --> LOCK BASE --> THERMO BASE --> POWER UIM --> PANEL UIM --> OVERLAY %% Utility dependencies TA --> CONST FACTORY --> CONST BASE --> HELP WSM --> CONST

Module Responsibilities

Module File Responsibility
TilesApp TilesApp.js Application orchestrator: initializes all subsystems, coordinates startup sequence, manages application lifecycle
HubitatAPI HubitatAPI.js REST API wrapper: device commands, mode management, request cancellation, multi-hub support
RequestQueue RequestQueue.js Request throttling and queuing to prevent API flooding
SettingsManager SettingsManager.js Configuration loader: parses settings.json, validates required fields, builds API URLs
DeviceFactory DeviceFactory.js Factory pattern implementation: determines device types, instantiates appropriate device classes
DeviceStateManager DeviceStateManager.js Centralized state store: pub/sub for device attribute changes, maintains canonical device state
BaseDevice BaseDevice.js Abstract base class: common device operations, attribute management, modal controls, command sending
WebSocketManager WebSocketManager.js Real-time updates: WebSocket connection, event parsing, reconnection logic, state propagation
UIManager UIManager.js UI coordinator: renders devices to panels, manages modes dropdown, coordinates overlay display
PanelManager PanelManager.js Panel visibility: toggle buttons for device categories, panel show/hide state
OverlayManager OverlayManager.js Loading/error overlays: full-screen messages during load or errors

5. Device Classification System

Classification Logic: Device type determination follows a priority-based algorithm that examines capabilities and naming conventions. The key insight is that device naming (e.g., "light" in label) takes precedence over raw capabilities for user-facing categorization, ensuring lights with dimming capability appear in the Lights panel rather than Dimmers.
flowchart TD START["Device Data from API"] --> LOCK_CHECK{"Has Lock capability?"} LOCK_CHECK -->|Yes| LOCK_TYPE["LOCK"] LOCK_CHECK -->|No| LIGHT_CHECK{"Name/label contains 'light'?"} LIGHT_CHECK -->|Yes| LIGHT_SWITCH{"Has Switch or SwitchLevel?"} LIGHT_CHECK -->|No| DIMMER_CHECK{"Has SwitchLevel?"} LIGHT_SWITCH -->|Yes| LIGHT_TYPE["LIGHT"] LIGHT_SWITCH -->|No| OTHER DIMMER_CHECK -->|Yes| DIMMER_TYPE["DIMMER"] DIMMER_CHECK -->|No| SWITCH_CHECK{"Has Switch only?"} SWITCH_CHECK -->|Yes| SWITCH_TYPE["SWITCH"] SWITCH_CHECK -->|No| THERMO_CHECK{"Has Thermostat?"} THERMO_CHECK -->|Yes| THERMO_TYPE["THERMOSTAT"] THERMO_CHECK -->|No| POWER_CHECK{"Has PowerMeter only?"} POWER_CHECK -->|Yes| POWER_TYPE["POWER_METER"] POWER_CHECK -->|No| OTHER["Unsupported Device"] %% Dual Creation Path LIGHT_TYPE --> DUAL_CHECK{"Also has SwitchLevel?"} DUAL_CHECK -->|Yes| DUAL["Create BOTH LightDevice AND DimmerDevice"] DUAL_CHECK -->|No| SINGLE_LIGHT["Create LightDevice only"] %% Styling classDef decision fill:#fff3cd,stroke:#856404 classDef deviceType fill:#d4edda,stroke:#155724 classDef special fill:#cce5ff,stroke:#004085 class LOCK_CHECK,LIGHT_CHECK,LIGHT_SWITCH,DIMMER_CHECK,SWITCH_CHECK,THERMO_CHECK,POWER_CHECK,DUAL_CHECK decision class LOCK_TYPE,LIGHT_TYPE,DIMMER_TYPE,SWITCH_TYPE,THERMO_TYPE,POWER_TYPE deviceType class DUAL,SINGLE_LIGHT special

Device Type Priority Order

// Priority order for device classification (DeviceFactory._determineDeviceType) const isLock = capabilities.includes("Lock"); const isLight = name.toLowerCase().includes("light") || label.toLowerCase().includes("light"); const isSwitchLevel = capabilities.includes("SwitchLevel"); const isSwitch = hasSwitchCapability && !isSwitchLevel; const isThermostat = capabilities.includes("Thermostat"); const isPowerMeterOnly = capabilities.includes("PowerMeter") && !hasSwitchCapability; // Classification priority: // 1. Lock capability → LOCKS panel // 2. Name/label "light" AND Switch/SwitchLevel → LIGHTS panel // 3. SwitchLevel (non-light) → DIMMERS panel // 4. Switch only → SWITCHES panel // 5. Thermostat → THERMOSTATS panel // 6. PowerMeter only → Power display in nav
Dual-Panel Devices: Dimmable lights (devices with "light" in name AND SwitchLevel capability) are instantiated twice: once as a LightDevice for the Lights panel (simple on/off button) and once as a DimmerDevice for the Dimmers panel (tile with level control modal). This provides both quick toggle access and detailed control without duplicating business logic.

Device Class Hierarchy

Class Extends Panel UI Element Key Features
BaseDevice Abstract base: ID, label, attributes, commands, state subscription, modal infrastructure
LightDevice BaseDevice Lights Button Simple on/off toggle with state-based styling
DimmerDevice BaseDevice Dimmers Tile + Modal Level display tile, vertical slider modal, +/- buttons
SwitchDevice BaseDevice Switches Button On/off toggle for non-light switches
LockDevice BaseDevice Locks Button Lock/unlock with Bootstrap Icons, state-based colors
ThermostatDevice BaseDevice Thermostats Circular Slider roundSlider control, mode buttons, turbo support, temperature tooltip
PowerMeterDevice BaseDevice Nav Bar Badge Real-time wattage display, main meter highlight

6. State Management

Publish-Subscribe Pattern: The DeviceStateManager implements a centralized state store with subscription-based change notification. Device instances subscribe to their own state changes during construction, enabling automatic UI updates when WebSocket events arrive.
sequenceDiagram participant WS as WebSocketManager participant DSM as DeviceStateManager participant DEV as Device Instance participant UI as Device UI Element Note over WS,UI: Initial Subscription (during device creation) DEV->>DSM: subscribe(deviceId, callback) DSM-->>DEV: returns unsubscribe function Note over WS,UI: Real-time Update Flow WS->>WS: Receive WebSocket message WS->>DSM: updateAttribute(deviceId, name, value) DSM->>DSM: Update internal state DSM->>DEV: Notify via callback(name, value) DEV->>DEV: updateAttribute(name, value) DEV->>UI: updateUI() UI->>UI: Reflect new state visually

State Flow Implementation

// DeviceStateManager: Subscription registration subscribe(deviceId, callback) { if (!this.listeners.has(deviceId)) { this.listeners.set(deviceId, []); } this.listeners.get(deviceId).push(callback); // Return unsubscribe function for cleanup return () => { const callbacks = this.listeners.get(deviceId); const index = callbacks.indexOf(callback); if (index > -1) callbacks.splice(index, 1); }; } // BaseDevice: Auto-subscription during construction _setupStateListener() { this.stateManager.subscribe(this.getId(), (attributeName, value) => { this.updateAttribute(attributeName, value); this.updateUI(); // Subclass implementation }); }

Attribute Update Handling

The state manager handles both array-format and object-format attributes from the Hubitat API, ensuring compatibility across different device types and API responses:

// Handle both attribute formats from Hubitat API updateAttribute(deviceId, attributeName, value) { const device = this.devices.get(deviceId); if (Array.isArray(device.attributes)) { // Array format: [{name: "switch", currentValue: "on"}, ...] const attr = device.attributes.find(a => a.name === attributeName); if (attr) { attr.currentValue = value; } else { device.attributes.push({ name: attributeName, currentValue: value }); } } else { // Object format: {switch: "on", level: 75, ...} device.attributes[attributeName] = value; } this._notifyListeners(deviceId, attributeName, value); }

7. Real-Time Communication

WebSocket Architecture: The system connects to Hubitat's native event socket (ws://hub-ip/eventsocket) for real-time device state updates. This eliminates polling overhead and provides instant UI synchronization when device states change through any control method (physical, app, automation).
flowchart LR subgraph HUBITAT["Hubitat Hub"] PHY["Physical Device"] DRIVER["Device Driver"] EVTSOCK["Event Socket
ws://:80/eventsocket"] end subgraph TILES["TILES Application"] WSM["WebSocketManager"] HANDLER["Event Handler"] DSM["DeviceStateManager"] DEVICE["Device Instance"] DOM["DOM Element"] end PHY -->|"State Change"| DRIVER DRIVER -->|"Event Publish"| EVTSOCK EVTSOCK -->|"JSON Message"| WSM WSM -->|"Parse Event"| HANDLER HANDLER -->|"updateAttribute()"| DSM DSM -->|"Notify Subscribers"| DEVICE DEVICE -->|"updateUI()"| DOM %% Styling classDef hubitat fill:#e0e0e0,stroke:#424242 classDef tiles fill:#e3f2fd,stroke:#1976d2 class PHY,DRIVER,EVTSOCK hubitat class WSM,HANDLER,DSM,DEVICE,DOM tiles

WebSocket Event Types

Event Name Source Devices Handler Action
switch Lights, Switches, Dimmers Update on/off state, trigger updateUI()
level Dimmers, Lights with SwitchLevel Update brightness level, call setValue()
lock Locks Update locked/unlocked state
power PowerMeter devices Update wattage display
thermostatMode Thermostats Update mode button active states
thermostatSetpoint Thermostats Update slider value and tooltip
temperature Thermostats Update current temperature in tooltip
turboMode Thermostats (if supported) Update turbo button state

Reconnection Strategy

// WebSocket reconnection with exponential backoff const WEBSOCKET = { MAX_RECONNECT_ATTEMPTS: 5, RECONNECT_MULTIPLIER: 1 }; _handleReconnect() { if (this.reconnectAttempts >= WEBSOCKET.MAX_RECONNECT_ATTEMPTS) { console.error('Max reconnection attempts reached'); return; } this.reconnectAttempts++; const delay = TIMEOUTS.WEBSOCKET_RECONNECT_BASE * this.reconnectAttempts; // Base: 5000ms, scales with attempts this.reconnectTimeout = setTimeout(() => { this._createConnection(); }, delay); }

8. API Integration

Hubitat Maker API: All device commands and state queries flow through Hubitat's Maker API. The HubitatAPI module wraps axios for HTTP requests with built-in timeout handling, request cancellation, and multi-hub support for coordinated mode changes.

API Endpoint Structure

// Base URL construction http://{hub_ip}/apps/api/{app_number}/{endpoint}?access_token={token} // Example endpoints: /devices/all // Get all devices /devices/{id} // Get device details /devices/{id}/{command} // Send command (on, off, toggle) /devices/{id}/{command}/{value} // Send command with value (setLevel/75) /modes/all // Get all location modes /modes/{id} // Set active mode

Request Handling

Method Endpoint Purpose Features
getAllDevices() /devices/all Initial device load Sorts by label alphabetically
getDevice(id) /devices/{id} Single device refresh Cancellable request token
sendCommand(id, cmd, val) /devices/{id}/{cmd}/{val} Device control 15s timeout, cancellation support
getModes() /modes/all Load location modes Identifies active mode
setMode(id) /modes/{id} Change location mode Propagates to all configured hubs

Multi-Hub Mode Synchronization

// setMode propagates to all configured hubs async setMode(modeId) { // Set on primary hub await axios.get(this.settings.buildURL(`/modes/${modeId}`)); // Propagate to other configured hubs await this._setModeOnOtherHubs(modeId); } _setModeOnOtherHubs(modeId) { const otherHubs = this.settings.getOtherHubs(); return Promise.all(otherHubs.map(hub => { const url = this.settings.buildHubURL(hub, `/modes/${modeId}`); return axios.get(url).catch(err => { console.error(`Failed to set mode on hub ${hub.ip}`); return null; // Continue with other hubs }); })); }

9. UI Component Architecture

Panel-Based Organization: Devices are rendered into category-specific panels (Lights, Switches, Dimmers, Locks, Thermostats). The PanelManager handles visibility toggling via navigation buttons, while UIManager coordinates rendering and overlay display.

Panel Structure

Panel Container ID Toggle Button Device Types
Lights #lights / #lightsCol #lightsToggle LightDevice instances
Switches #switches / #otherSwitchesCol #switchesToggle SwitchDevice instances
Dimmers #dimmers / #dimmersCol #dimmersToggle DimmerDevice instances
Locks #rowLocks / #locksCol #locksToggle LockDevice instances
Thermostats #thermostats / #thermostatsCol #thremostatsToggle ThermostatDevice instances

Mobile Modal System

Touch devices trigger modal overlays for detailed device control. The BaseDevice class provides shared modal infrastructure including vertical sliders for dimmers and Bootstrap modals for thermostats:

// BaseDevice modal infrastructure (used by DimmerDevice, ThermostatDevice) _createModalHTML(currentLevel) { const modalBackdrop = $('<div>') .addClass('dimmer-modal-backdrop') .attr('data-device-id', this.getId()); const sliderContainer = this._createVerticalSlider(currentLevel); // HomeKit-style vertical slider with +/- buttons return modalBackdrop; } // Touch/drag handling for slider interaction _handleSliderInteraction(e, track) { const clientY = e.type.includes('touch') ? e.originalEvent.touches[0].clientY : e.clientY; const position = (clientY - trackOffset.top) / trackHeight; const level = Math.round((1 - position) * 99); // Invert for bottom-up fill this._updateModalSlider(level); }

Thermostat Control Interface

The ThermostatDevice implements a circular roundSlider control with mode buttons and temperature tooltip:

// roundSlider initialization with dynamic radius _initializeSlider(setpoint, currentTemp, outdoorTemp) { const containerWidth = sliderElement.width(); const calculatedRadius = Math.floor(containerWidth / 2); sliderElement.roundSlider({ sliderType: "min-range", radius: calculatedRadius, min: 62, max: 86, startAngle: 315, tooltipFormat: (args) => this._formatTooltip(args.value, currentTemp, outdoorTemp), change: (evt) => this._handleTemperatureChange(evt) }); } // Mode-aware temperature command async _handleTemperatureChange(evt) { const mode = this.getAttribute('thermostatMode'); if (mode === 'cool') { await this.sendCommand('setCoolingSetpoint', evt.value); } else if (mode === 'heat') { await this.sendCommand('setHeatingSetpoint', evt.value); } else { // Auto mode: set both setpoints await this.sendCommand('setHeatingSetpoint', evt.value); await this.sendCommand('setCoolingSetpoint', evt.value); } }

10. Deployment Architecture

Containerized Deployment: The application runs in a Docker container with Flask/Gunicorn serving static files. The container exposes port 80 for direct HTTP access without reverse proxy requirements.
graph TB subgraph NETWORK["Local Network"] CLIENT["Browser Client"] HUBITAT["Hubitat Hub
:80 HTTP
:80 WebSocket"] end subgraph SERVER["Docker Host (Ubuntu)"] subgraph CONTAINER["tiles-app Container"] GUNICORN["Gunicorn WSGI"] FLASK["Flask App"] STATIC["Static Files
/app/app/"] end end CLIENT -->|":80 HTTP"| GUNICORN GUNICORN --> FLASK FLASK --> STATIC CLIENT <-->|"REST API"| HUBITAT CLIENT <-->|"WebSocket"| HUBITAT %% Styling classDef client fill:#e3f2fd,stroke:#1976d2 classDef server fill:#e8f5e9,stroke:#388e3c classDef hubitat fill:#fff3e0,stroke:#f57c00 class CLIENT client class GUNICORN,FLASK,STATIC server class HUBITAT hubitat

Docker Configuration

# docker-compose.yml name: tiles services: tiles: build: context: . dockerfile: Dockerfile container_name: tiles-app ports: - "80:80" restart: unless-stopped volumes: - ./app:/app/app # Live reload during development

Network Requirements

Connection Protocol Port Purpose
Client → TILES HTTP 80 Static file serving (HTML, JS, CSS)
Client → Hubitat HTTP 80 Maker API REST calls
Client → Hubitat WebSocket 80 Event socket for real-time updates

11. Configuration System

External Configuration: All hub-specific settings are stored in settings.json, keeping credentials and network configuration separate from application code. The SettingsManager validates required fields during load and provides URL building utilities.

settings.json Structure

{ "access_token": "your-maker-api-access-token", "ip": "192.168.1.100", "appNumber": "123", // Optional: Additional hubs for mode synchronization "otherHubs": { "hub2": "192.168.1.101", "hub3": "192.168.1.102" }, "otherHubsAppNumbers": { "hub2": "124", "hub3": "125" }, "otherHubsTokens": { "hub2": "token-for-hub2", "hub3": "token-for-hub3" }, // Optional settings "showOfflineDevices": false }

Constants Configuration

Application constants are centralized in constants.js for easy maintenance:

Category Constant Value Purpose
Timeouts REQUEST_TIMEOUT 15000ms API request timeout
Timeouts RELOAD_PAGE 10 hours Automatic page refresh interval
Timeouts WEBSOCKET_RECONNECT_BASE 5000ms Base delay for reconnection attempts
WebSocket MAX_RECONNECT_ATTEMPTS 5 Maximum reconnection tries before giving up
Thermostat Min/Max Temperature 62°F / 86°F Slider range limits

12. Key Design Decisions

Rationale Documentation: The following decisions were made deliberately based on project requirements, maintainability goals, and lessons learned during development.

1. ES6 Modules Over Bundlers

Decision: Use native ES6 modules with browser import/export rather than webpack or other bundlers.

Rationale: Modern browsers support ES6 modules natively. Avoiding build tooling reduces complexity, eliminates transpilation issues, and allows direct debugging of source files. The application size does not warrant the overhead of a build pipeline.

2. jQuery for DOM Manipulation

Decision: Retain jQuery despite ES6+ availability of native DOM methods.

Rationale: jQuery provides consistent cross-browser behavior, particularly for event delegation and animation. The roundSlider library requires jQuery. Existing codebase familiarity reduces refactoring risk.

3. Factory Pattern for Device Creation

Decision: Centralize device instantiation in DeviceFactory rather than inline construction.

Rationale: Device type determination logic is complex (priority-based with multiple conditions). Centralizing this logic makes it testable and maintainable. The factory also handles the dual-instantiation case for dimmable lights.

4. Pub/Sub State Management

Decision: Implement a custom publish-subscribe pattern in DeviceStateManager rather than using a state management library (Redux, MobX).

Rationale: The state model is simple (device attributes keyed by ID). A lightweight custom implementation avoids dependency bloat and integrates naturally with the device class hierarchy. Devices self-subscribe during construction.

5. Device Classification Priority

Decision: Name/label-based classification ("light" keyword) takes precedence over capability-only detection.

Rationale: Users expect devices named "Kitchen Light" to appear in the Lights panel even if they have dimmer capability. Pure capability detection would scatter lights across Lights and Dimmers panels. The "dimmable light creates two instances" pattern preserves both organizational clarity and full functionality.

6. Optimistic UI Updates

Decision: Update UI immediately on user interaction, before server confirmation.

Rationale: Provides responsive feel on touch devices. WebSocket events correct any discrepancies within milliseconds. The alternative (wait for server response) creates noticeable lag that degrades user experience.

7. Automatic Page Reload

Decision: Force page reload every 10 hours.

Rationale: Long-running browser sessions can accumulate memory leaks or stale state. A periodic refresh ensures clean state. The 10-hour interval balances freshness with avoiding disruptive reloads during active use.

13. Project Structure

/home/elfege/0_TILES/ ├── app/ │ ├── index.html // Main entry point │ ├── tiles-modular.js // ES6 module bootstrap │ ├── tiles.css // Main stylesheet │ ├── mobile.css // Mobile-specific styles │ ├── settings.json // Hub configuration │ ├── roundslider.js/css // Thermostat slider library │ └── modules/ │ ├── TilesApp.js // Application orchestrator │ ├── api/ │ │ ├── HubitatAPI.js // REST API wrapper │ │ └── RequestQueue.js // Request throttling │ ├── config/ │ │ └── SettingsManager.js // Configuration loader │ ├── devices/ │ │ ├── BaseDevice.js // Abstract base class │ │ ├── DeviceFactory.js // Factory pattern │ │ ├── LightDevice.js // Light implementation │ │ ├── DimmerDevice.js // Dimmer implementation │ │ ├── SwitchDevice.js // Switch implementation │ │ ├── LockDevice.js // Lock implementation │ │ ├── ThermostatDevice.js// Thermostat implementation │ │ └── PowerMeterDevice.js// Power meter implementation │ ├── state/ │ │ └── DeviceStateManager.js // Pub/sub state store │ ├── ui/ │ │ ├── UIManager.js // UI coordinator │ │ ├── PanelManager.js // Panel visibility │ │ └── OverlayManager.js // Loading/error overlays │ ├── utils/ │ │ ├── constants.js // App constants │ │ └── helpers.js // Utility functions │ └── websocket/ │ └── WebSocketManager.js// Real-time connection ├── app.py // Flask application ├── docker-compose.yml // Container configuration ├── gunicorn_conf.py // Production WSGI config ├── requirements.txt // Python dependencies └── DOCS/ └── README_project_history.md // Development history