Răsfoiți Sursa

Add table type override setting and collapsible settings sections

- Add table_type_override to allow manual override of auto-detected machine type
- Override affects gear ratio (6.25 for mini variants, 10 for standard) and homing behavior
- Add Machine Settings section to settings page with dropdown for table type selection
- Make all settings sections collapsible with toggle headers
- Device Connection expanded by default, all other sections collapsed
- Smooth CSS transitions for expand/collapse animations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 6 luni în urmă
părinte
comite
28f966d310
5 a modificat fișierele cu 310 adăugiri și 33 ștergeri
  1. 25 0
      main.py
  2. 12 6
      modules/connection/connection_manager.py
  3. 3 0
      modules/core/state.py
  4. 123 0
      static/js/settings.js
  5. 147 27
      templates/settings.html

+ 25 - 0
main.py

@@ -387,6 +387,9 @@ class MqttSettingsUpdate(BaseModel):
     device_id: Optional[str] = None
     device_id: Optional[str] = None
     device_name: Optional[str] = None
     device_name: Optional[str] = None
 
 
+class MachineSettingsUpdate(BaseModel):
+    table_type_override: Optional[str] = None  # Override detected table type, or empty string/"auto" to clear
+
 class SettingsUpdate(BaseModel):
 class SettingsUpdate(BaseModel):
     """Request model for PATCH /api/settings - all fields optional for partial updates"""
     """Request model for PATCH /api/settings - all fields optional for partial updates"""
     app: Optional[AppSettingsUpdate] = None
     app: Optional[AppSettingsUpdate] = None
@@ -397,6 +400,7 @@ class SettingsUpdate(BaseModel):
     homing: Optional[HomingSettingsUpdate] = None
     homing: Optional[HomingSettingsUpdate] = None
     led: Optional[LedSettingsUpdate] = None
     led: Optional[LedSettingsUpdate] = None
     mqtt: Optional[MqttSettingsUpdate] = None
     mqtt: Optional[MqttSettingsUpdate] = None
+    machine: Optional[MachineSettingsUpdate] = None
 
 
 # Store active WebSocket connections
 # Store active WebSocket connections
 active_status_connections = set()
 active_status_connections = set()
@@ -553,6 +557,19 @@ async def get_all_settings():
             "discovery_prefix": state.mqtt_discovery_prefix,
             "discovery_prefix": state.mqtt_discovery_prefix,
             "device_id": state.mqtt_device_id,
             "device_id": state.mqtt_device_id,
             "device_name": state.mqtt_device_name
             "device_name": state.mqtt_device_name
+        },
+        "machine": {
+            "detected_table_type": state.table_type,
+            "table_type_override": state.table_type_override,
+            "effective_table_type": state.table_type_override or state.table_type,
+            "available_table_types": [
+                {"value": "dune_weaver_mini", "label": "Dune Weaver Mini"},
+                {"value": "dune_weaver_mini_pro", "label": "Dune Weaver Mini Pro"},
+                {"value": "dune_weaver_mini_pro_byj", "label": "Dune Weaver Mini Pro (BYJ)"},
+                {"value": "dune_weaver_gold", "label": "Dune Weaver Gold"},
+                {"value": "dune_weaver", "label": "Dune Weaver"},
+                {"value": "dune_weaver_pro", "label": "Dune Weaver Pro"}
+            ]
         }
         }
     }
     }
 
 
@@ -704,6 +721,14 @@ async def update_settings(settings_update: SettingsUpdate):
         updated_categories.append("mqtt")
         updated_categories.append("mqtt")
         requires_restart = True
         requires_restart = True
 
 
+    # Machine settings
+    if settings_update.machine:
+        m = settings_update.machine
+        if m.table_type_override is not None:
+            # Empty string or "auto" clears the override
+            state.table_type_override = None if m.table_type_override in ("", "auto") else m.table_type_override
+        updated_categories.append("machine")
+
     # Save state
     # Save state
     state.save()
     state.save()
 
 

+ 12 - 6
modules/connection/connection_manager.py

@@ -517,8 +517,11 @@ def get_machine_steps(timeout=10):
             state.table_type = None
             state.table_type = None
             logger.warning(f"Unknown table type with Y steps/mm: {y_steps_per_mm}")
             logger.warning(f"Unknown table type with Y steps/mm: {y_steps_per_mm}")
 
 
-        # Set gear ratio based on table type (hardcoded)
-        if state.table_type in ['dune_weaver_mini', 'dune_weaver_mini_pro', 'dune_weaver_mini_pro_byj', 'dune_weaver_gold']:
+        # Use override if set, otherwise use detected table type
+        effective_table_type = state.table_type_override or state.table_type
+
+        # Set gear ratio based on effective table type (hardcoded)
+        if effective_table_type in ['dune_weaver_mini', 'dune_weaver_mini_pro', 'dune_weaver_mini_pro_byj', 'dune_weaver_gold']:
             state.gear_ratio = 6.25
             state.gear_ratio = 6.25
         else:
         else:
             state.gear_ratio = 10
             state.gear_ratio = 10
@@ -528,10 +531,12 @@ def get_machine_steps(timeout=10):
         if gear_ratio_override is not None:
         if gear_ratio_override is not None:
             try:
             try:
                 state.gear_ratio = float(gear_ratio_override)
                 state.gear_ratio = float(gear_ratio_override)
-                logger.info(f"Machine type detected: {state.table_type}, gear ratio: {state.gear_ratio} (from GEAR_RATIO env var)")
+                logger.info(f"Machine type detected: {state.table_type}, effective: {effective_table_type}, gear ratio: {state.gear_ratio} (from GEAR_RATIO env var)")
             except ValueError:
             except ValueError:
                 logger.error(f"Invalid GEAR_RATIO env var value: {gear_ratio_override}, using default: {state.gear_ratio}")
                 logger.error(f"Invalid GEAR_RATIO env var value: {gear_ratio_override}, using default: {state.gear_ratio}")
-                logger.info(f"Machine type detected: {state.table_type}, gear ratio: {state.gear_ratio} (hardcoded)")
+                logger.info(f"Machine type detected: {state.table_type}, effective: {effective_table_type}, gear ratio: {state.gear_ratio} (hardcoded)")
+        elif state.table_type_override:
+            logger.info(f"Machine type detected: {state.table_type}, overridden to: {effective_table_type}, gear ratio: {state.gear_ratio}")
         else:
         else:
             logger.info(f"Machine type detected: {state.table_type}, gear ratio: {state.gear_ratio} (hardcoded)")
             logger.info(f"Machine type detected: {state.table_type}, gear ratio: {state.gear_ratio} (hardcoded)")
 
 
@@ -574,8 +579,9 @@ def home(timeout=90):
 
 
     def home_internal():
     def home_internal():
         nonlocal homing_success
         nonlocal homing_success
+        effective_table_type = state.table_type_override or state.table_type
         homing_speed = 400
         homing_speed = 400
-        if state.table_type == 'dune_weaver_mini':
+        if effective_table_type == 'dune_weaver_mini':
             homing_speed = 100
             homing_speed = 100
         try:
         try:
             if state.homing == 1:
             if state.homing == 1:
@@ -670,7 +676,7 @@ def home(timeout=90):
                 loop = asyncio.new_event_loop()
                 loop = asyncio.new_event_loop()
                 asyncio.set_event_loop(loop)
                 asyncio.set_event_loop(loop)
                 try:
                 try:
-                    if state.table_type == 'dune_weaver_mini':
+                    if effective_table_type == 'dune_weaver_mini':
                         result = loop.run_until_complete(send_grbl_coordinates(0, -30, homing_speed, home=True))
                         result = loop.run_until_complete(send_grbl_coordinates(0, -30, homing_speed, home=True))
                         if result == False:
                         if result == False:
                             logger.error("Crash homing failed - send_grbl_coordinates returned False")
                             logger.error("Crash homing failed - send_grbl_coordinates returned False")

+ 3 - 0
modules/core/state.py

@@ -84,6 +84,7 @@ class AppState:
         self.dw_led_last_activity_time = None  # Last activity timestamp (runtime only, not persisted)
         self.dw_led_last_activity_time = None  # Last activity timestamp (runtime only, not persisted)
         self.skip_requested = False
         self.skip_requested = False
         self.table_type = None
         self.table_type = None
+        self.table_type_override = None  # User override for table type detection
         self._playlist_mode = "loop"
         self._playlist_mode = "loop"
         self._pause_time = 0
         self._pause_time = 0
         self._clear_pattern = "none"
         self._clear_pattern = "none"
@@ -285,6 +286,7 @@ class AppState:
             "mqtt_discovery_prefix": self.mqtt_discovery_prefix,
             "mqtt_discovery_prefix": self.mqtt_discovery_prefix,
             "mqtt_device_id": self.mqtt_device_id,
             "mqtt_device_id": self.mqtt_device_id,
             "mqtt_device_name": self.mqtt_device_name,
             "mqtt_device_name": self.mqtt_device_name,
+            "table_type_override": self.table_type_override,
         }
         }
 
 
     def from_dict(self, data):
     def from_dict(self, data):
@@ -369,6 +371,7 @@ class AppState:
         self.mqtt_discovery_prefix = data.get("mqtt_discovery_prefix", "homeassistant")
         self.mqtt_discovery_prefix = data.get("mqtt_discovery_prefix", "homeassistant")
         self.mqtt_device_id = data.get("mqtt_device_id", "dune_weaver")
         self.mqtt_device_id = data.get("mqtt_device_id", "dune_weaver")
         self.mqtt_device_name = data.get("mqtt_device_name", "Dune Weaver")
         self.mqtt_device_name = data.get("mqtt_device_name", "Dune Weaver")
+        self.table_type_override = data.get("table_type_override", None)
 
 
     def save(self):
     def save(self):
         """Save the current state to a JSON file."""
         """Save the current state to a JSON file."""

+ 123 - 0
static/js/settings.js

@@ -1,3 +1,25 @@
+// ============================================================================
+// Collapsible Section Toggle
+// ============================================================================
+
+function toggleSection(headerElement) {
+    const contentElement = headerElement.nextElementSibling;
+
+    if (headerElement.classList.contains('collapsed')) {
+        // Expand
+        headerElement.classList.remove('collapsed');
+        contentElement.classList.remove('collapsed');
+    } else {
+        // Collapse
+        headerElement.classList.add('collapsed');
+        contentElement.classList.add('collapsed');
+    }
+}
+
+// ============================================================================
+// Constants and Utilities
+// ============================================================================
+
 // Constants for log message types
 // Constants for log message types
 const LOG_TYPE = {
 const LOG_TYPE = {
     SUCCESS: 'success',
     SUCCESS: 'success',
@@ -2282,4 +2304,105 @@ async function initializeMqttConfig() {
 // Initialize MQTT config when DOM is ready
 // Initialize MQTT config when DOM is ready
 document.addEventListener('DOMContentLoaded', function() {
 document.addEventListener('DOMContentLoaded', function() {
     initializeMqttConfig();
     initializeMqttConfig();
+    initializeTableTypeConfig();
 });
 });
+
+// ============================================================================
+// Table Type Configuration
+// ============================================================================
+
+function initializeTableTypeConfig() {
+    const tableTypeSelect = document.getElementById('tableTypeSelect');
+    const saveTableTypeButton = document.getElementById('saveTableType');
+    const detectedTableType = document.getElementById('detectedTableType');
+
+    if (!tableTypeSelect || !saveTableTypeButton) {
+        logMessage('Table type elements not found', LOG_TYPE.WARNING);
+        return;
+    }
+
+    // Load current settings
+    loadTableTypeSettings();
+
+    // Save button click handler
+    saveTableTypeButton.addEventListener('click', saveTableTypeConfig);
+
+    async function loadTableTypeSettings() {
+        try {
+            const response = await fetch('/api/settings');
+            if (!response.ok) throw new Error('Failed to fetch settings');
+
+            const settings = await response.json();
+            const machine = settings.machine || {};
+
+            // Populate dropdown with available table types
+            tableTypeSelect.innerHTML = '<option value="">Auto-detect (use detected type)</option>';
+            if (machine.available_table_types) {
+                machine.available_table_types.forEach(type => {
+                    const option = document.createElement('option');
+                    option.value = type.value;
+                    option.textContent = type.label;
+                    tableTypeSelect.appendChild(option);
+                });
+            }
+
+            // Set current override value
+            if (machine.table_type_override) {
+                tableTypeSelect.value = machine.table_type_override;
+            } else {
+                tableTypeSelect.value = '';
+            }
+
+            // Update detected type display
+            if (detectedTableType) {
+                const detected = machine.detected_table_type;
+                if (detected) {
+                    // Find the label for the detected type
+                    const typeInfo = machine.available_table_types?.find(t => t.value === detected);
+                    detectedTableType.textContent = typeInfo ? typeInfo.label : detected;
+                } else {
+                    detectedTableType.textContent = 'Not connected';
+                }
+            }
+
+            logMessage('Table type settings loaded', LOG_TYPE.DEBUG);
+        } catch (error) {
+            logMessage(`Error loading table type settings: ${error.message}`, LOG_TYPE.ERROR);
+        }
+    }
+
+    async function saveTableTypeConfig() {
+        const originalButtonHTML = saveTableTypeButton.innerHTML;
+        saveTableTypeButton.disabled = true;
+        saveTableTypeButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Saving...</span>';
+
+        try {
+            const response = await fetch('/api/settings', {
+                method: 'PATCH',
+                headers: { 'Content-Type': 'application/json' },
+                body: JSON.stringify({
+                    machine: {
+                        table_type_override: tableTypeSelect.value || ''
+                    }
+                })
+            });
+
+            if (!response.ok) throw new Error('Failed to save settings');
+
+            const result = await response.json();
+            if (result.success) {
+                showStatusMessage('Table type settings saved. Changes will take effect on next connection.', 'success');
+                // Reload to show updated effective type
+                await loadTableTypeSettings();
+            } else {
+                throw new Error('Save failed');
+            }
+        } catch (error) {
+            logMessage(`Error saving table type: ${error.message}`, LOG_TYPE.ERROR);
+            showStatusMessage(`Failed to save table type: ${error.message}`, 'error');
+        } finally {
+            saveTableTypeButton.innerHTML = originalButtonHTML;
+            saveTableTypeButton.disabled = false;
+        }
+    }
+}

+ 147 - 27
templates/settings.html

@@ -203,6 +203,49 @@ input:checked + .slider:before {
   animation: spin 1s linear infinite;
   animation: spin 1s linear infinite;
 }
 }
 
 
+/* Collapsible section styles */
+.section-header {
+  cursor: pointer;
+  user-select: none;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.section-header:hover {
+  background-color: #f8fafc;
+}
+
+.dark .section-header:hover {
+  background-color: #333333;
+}
+
+.section-toggle-icon {
+  transition: transform 0.2s ease-in-out;
+}
+
+.section-header.collapsed .section-toggle-icon {
+  transform: rotate(-90deg);
+}
+
+.section-header.collapsed {
+  border-bottom: none;
+}
+
+.section-content {
+  overflow: hidden;
+  transition: max-height 0.3s ease-in-out, opacity 0.2s ease-in-out, padding 0.2s ease-in-out;
+  max-height: 2000px;
+  opacity: 1;
+}
+
+.section-content.collapsed {
+  max-height: 0;
+  opacity: 0;
+  padding-top: 0 !important;
+  padding-bottom: 0 !important;
+}
+
 /* Time slot specific styles */
 /* Time slot specific styles */
 .time-slot-item {
 .time-slot-item {
   background-color: #f8fafc;
   background-color: #f8fafc;
@@ -303,11 +346,13 @@ input:checked + .slider:before {
   </div>
   </div>
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
     >
-      Device Connection
+      <span>Device Connection</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
     </h2>
-    <div>
+    <div class="section-content">
       <div
       <div
         class="flex items-center gap-4 px-6 py-5 hover:bg-slate-50 transition-colors"
         class="flex items-center gap-4 px-6 py-5 hover:bg-slate-50 transition-colors"
       >
       >
@@ -393,14 +438,75 @@ input:checked + .slider:before {
       </div>
       </div>
     </div>
     </div>
   </section>
   </section>
+  <!-- Machine Settings Section -->
+  <section id="machineSection" class="bg-white rounded-xl shadow-sm overflow-hidden">
+    <h2
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
+    >
+      <span>Machine Settings</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
+    </h2>
+    <div class="section-content collapsed px-6 py-5 space-y-6">
+      <!-- Table Type Override -->
+      <div class="space-y-3">
+        <label class="text-sm font-medium text-slate-700 flex items-center gap-2">
+          <span class="material-icons text-slate-600 text-base">precision_manufacturing</span>
+          Table Type
+        </label>
+
+        <!-- Current detected type display -->
+        <div id="detectedTableTypeContainer" class="flex items-center gap-2 text-sm text-slate-600 bg-slate-50 rounded-lg p-3">
+          <span class="material-icons text-slate-500 text-base">info</span>
+          <span>Detected: <span id="detectedTableType" class="font-medium text-slate-800">Unknown</span></span>
+        </div>
+
+        <div class="flex gap-3 items-center">
+          <select
+            id="tableTypeSelect"
+            class="form-select flex-1 resize-none overflow-hidden rounded-lg text-slate-900 focus:outline-0 focus:ring-2 focus:ring-sky-500 border border-slate-300 bg-white focus:border-sky-500 h-10 placeholder:text-slate-400 px-4 text-base font-medium leading-normal transition-colors"
+          >
+            <option value="">Auto-detect (use detected type)</option>
+          </select>
+          <button
+            id="saveTableType"
+            class="flex items-center justify-center gap-2 min-w-[100px] cursor-pointer rounded-lg h-10 px-4 bg-sky-600 hover:bg-sky-700 text-white text-sm font-medium leading-normal tracking-[0.015em] transition-colors flex-shrink-0"
+          >
+            <span class="material-icons text-lg">save</span>
+            <span class="truncate">Save</span>
+          </button>
+        </div>
+        <p class="text-xs text-slate-500">
+          Override the automatically detected table type. This affects gear ratio calculations and homing behavior. Leave as "Auto-detect" unless you need to manually specify your table type.
+        </p>
+      </div>
+
+      <!-- Info box -->
+      <div class="text-xs text-slate-600 bg-blue-50 border border-blue-200 rounded-lg p-3">
+        <div class="flex items-start gap-2">
+          <span class="material-icons text-blue-600 text-base">info</span>
+          <div>
+            <p class="font-medium text-blue-800">Table Type Detection</p>
+            <ul class="mt-1 space-y-1 text-blue-700">
+              <li>• Table type is normally detected automatically from GRBL settings</li>
+              <li>• Use override if auto-detection is incorrect for your hardware</li>
+              <li>• Changes take effect on next connection/homing</li>
+            </ul>
+          </div>
+        </div>
+      </div>
+    </div>
+  </section>
   <!-- Homing Configuration Section -->
   <!-- Homing Configuration Section -->
   <section id="homingSection" class="bg-white rounded-xl shadow-sm overflow-hidden">
   <section id="homingSection" class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
     >
-      Homing Configuration
+      <span>Homing Configuration</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
     </h2>
-    <div class="px-6 py-5 space-y-6">
+    <div class="section-content collapsed px-6 py-5 space-y-6">
       <!-- Homing Mode Selection -->
       <!-- Homing Mode Selection -->
       <div class="space-y-3">
       <div class="space-y-3">
         <label class="text-sm font-medium text-slate-700 flex items-center gap-2">
         <label class="text-sm font-medium text-slate-700 flex items-center gap-2">
@@ -533,11 +639,13 @@ input:checked + .slider:before {
   </section>
   </section>
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
     >
-      Application Settings
+      <span>Application Settings</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
     </h2>
-    <div class="px-6 py-5 space-y-6">
+    <div class="section-content collapsed px-6 py-5 space-y-6">
       <label class="flex flex-col gap-1.5">
       <label class="flex flex-col gap-1.5">
         <span class="text-slate-700 text-sm font-medium leading-normal"
         <span class="text-slate-700 text-sm font-medium leading-normal"
           >Application Name</span
           >Application Name</span
@@ -613,11 +721,13 @@ input:checked + .slider:before {
   </section>
   </section>
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
     >
-      Pattern Clearing
+      <span>Pattern Clearing</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
     </h2>
-    <div class="px-6 py-5 space-y-6">
+    <div class="section-content collapsed px-6 py-5 space-y-6">
       <p class="text-sm text-slate-600">
       <p class="text-sm text-slate-600">
         Customize the clearing behavior used when transitioning between patterns. Set custom patterns and speed to control how sand is distributed.
         Customize the clearing behavior used when transitioning between patterns. Set custom patterns and speed to control how sand is distributed.
       </p>
       </p>
@@ -730,11 +840,13 @@ input:checked + .slider:before {
   </section>
   </section>
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
     >
-      LED Controller Configuration
+      <span>LED Controller Configuration</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
     </h2>
-    <div class="px-6 py-5 space-y-6">
+    <div class="section-content collapsed px-6 py-5 space-y-6">
       <!-- LED Provider Selection -->
       <!-- LED Provider Selection -->
       <div class="flex flex-col gap-2">
       <div class="flex flex-col gap-2">
         <span class="text-slate-700 text-sm font-medium leading-normal">LED Provider</span>
         <span class="text-slate-700 text-sm font-medium leading-normal">LED Provider</span>
@@ -865,11 +977,13 @@ input:checked + .slider:before {
   <!-- MQTT Configuration Section -->
   <!-- MQTT Configuration Section -->
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
     >
-      Home Assistant Integration
+      <span>Home Assistant Integration</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
     </h2>
-    <div class="px-6 py-5 space-y-6">
+    <div class="section-content collapsed px-6 py-5 space-y-6">
       <!-- MQTT Enable Toggle -->
       <!-- MQTT Enable Toggle -->
       <div class="flex items-center justify-between">
       <div class="flex items-center justify-between">
         <div class="flex-1">
         <div class="flex-1">
@@ -1057,11 +1171,13 @@ input:checked + .slider:before {
 
 
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
     >
-      Auto-play on Boot
+      <span>Auto-play on Boot</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
     </h2>
-    <div class="px-6 py-5 space-y-6">
+    <div class="section-content collapsed px-6 py-5 space-y-6">
       <div class="flex items-center justify-between">
       <div class="flex items-center justify-between">
         <div class="flex-1">
         <div class="flex-1">
           <h3 class="text-slate-700 text-base font-medium leading-normal">Enable Auto-play on Boot</h3>
           <h3 class="text-slate-700 text-base font-medium leading-normal">Enable Auto-play on Boot</h3>
@@ -1167,11 +1283,13 @@ input:checked + .slider:before {
   </section>
   </section>
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
   <section class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
     >
-      Still Sands
+      <span>Still Sands</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
     </h2>
-    <div class="px-6 py-5 space-y-6">
+    <div class="section-content collapsed px-6 py-5 space-y-6">
       <div class="flex items-center justify-between">
       <div class="flex items-center justify-between">
         <div class="flex-1">
         <div class="flex-1">
           <h3 class="text-slate-700 text-base font-medium leading-normal">Enable Still Sands</h3>
           <h3 class="text-slate-700 text-base font-medium leading-normal">Enable Still Sands</h3>
@@ -1364,11 +1482,13 @@ input:checked + .slider:before {
   </section>
   </section>
   <section id="software-version-section" class="bg-white rounded-xl shadow-sm overflow-hidden">
   <section id="software-version-section" class="bg-white rounded-xl shadow-sm overflow-hidden">
     <h2
     <h2
-      class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      class="section-header collapsed text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em] px-6 py-4 border-b border-slate-200"
+      onclick="toggleSection(this)"
     >
     >
-      Software Version
+      <span>Software Version</span>
+      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
     </h2>
     </h2>
-    <div>
+    <div class="section-content collapsed">
       <div class="flex items-center gap-4 px-6 py-5">
       <div class="flex items-center gap-4 px-6 py-5">
         <div
         <div
           class="text-slate-600 flex items-center justify-center rounded-lg bg-slate-100 shrink-0 size-12"
           class="text-slate-600 flex items-center justify-center rounded-lg bg-slate-100 shrink-0 size-12"