Selaa lähdekoodia

add timezone settings for still sands

tuanchris 2 kuukautta sitten
vanhempi
sitoutus
9c93fb69da
5 muutettua tiedostoa jossa 238 lisäystä ja 42 poistoa
  1. 19 1
      main.py
  2. 29 24
      modules/core/pattern_manager.py
  3. 3 0
      modules/core/state.py
  4. 52 12
      static/js/settings.js
  5. 135 5
      templates/settings.html

+ 19 - 1
main.py

@@ -269,6 +269,7 @@ class ScheduledPauseRequest(BaseModel):
     enabled: bool
     enabled: bool
     control_wled: Optional[bool] = False
     control_wled: Optional[bool] = False
     finish_pattern: Optional[bool] = False  # Finish current pattern before pausing
     finish_pattern: Optional[bool] = False  # Finish current pattern before pausing
+    timezone: Optional[str] = None  # IANA timezone or None for system default
     time_slots: List[TimeSlot] = []
     time_slots: List[TimeSlot] = []
 
 
 class CoordinateRequest(BaseModel):
 class CoordinateRequest(BaseModel):
@@ -348,6 +349,7 @@ class ScheduledPauseSettingsUpdate(BaseModel):
     enabled: Optional[bool] = None
     enabled: Optional[bool] = None
     control_wled: Optional[bool] = None
     control_wled: Optional[bool] = None
     finish_pattern: Optional[bool] = None
     finish_pattern: Optional[bool] = None
+    timezone: Optional[str] = None  # IANA timezone (e.g., "America/New_York") or None for system default
     time_slots: Optional[List[TimeSlot]] = None
     time_slots: Optional[List[TimeSlot]] = None
 
 
 class HomingSettingsUpdate(BaseModel):
 class HomingSettingsUpdate(BaseModel):
@@ -514,6 +516,7 @@ async def get_all_settings():
             "enabled": state.scheduled_pause_enabled,
             "enabled": state.scheduled_pause_enabled,
             "control_wled": state.scheduled_pause_control_wled,
             "control_wled": state.scheduled_pause_control_wled,
             "finish_pattern": state.scheduled_pause_finish_pattern,
             "finish_pattern": state.scheduled_pause_finish_pattern,
+            "timezone": state.scheduled_pause_timezone,
             "time_slots": state.scheduled_pause_time_slots
             "time_slots": state.scheduled_pause_time_slots
         },
         },
         "homing": {
         "homing": {
@@ -616,6 +619,13 @@ async def update_settings(settings_update: SettingsUpdate):
             state.scheduled_pause_control_wled = sp.control_wled
             state.scheduled_pause_control_wled = sp.control_wled
         if sp.finish_pattern is not None:
         if sp.finish_pattern is not None:
             state.scheduled_pause_finish_pattern = sp.finish_pattern
             state.scheduled_pause_finish_pattern = sp.finish_pattern
+        if sp.timezone is not None:
+            # Empty string means use system default (store as None)
+            state.scheduled_pause_timezone = sp.timezone if sp.timezone else None
+            # Clear cached timezone in pattern_manager so it picks up the new setting
+            from modules.core import pattern_manager
+            pattern_manager._cached_timezone = None
+            pattern_manager._cached_zoneinfo = None
         if sp.time_slots is not None:
         if sp.time_slots is not None:
             state.scheduled_pause_time_slots = [slot.model_dump() for slot in sp.time_slots]
             state.scheduled_pause_time_slots = [slot.model_dump() for slot in sp.time_slots]
         updated_categories.append("scheduled_pause")
         updated_categories.append("scheduled_pause")
@@ -748,6 +758,7 @@ async def get_scheduled_pause():
         "enabled": state.scheduled_pause_enabled,
         "enabled": state.scheduled_pause_enabled,
         "control_wled": state.scheduled_pause_control_wled,
         "control_wled": state.scheduled_pause_control_wled,
         "finish_pattern": state.scheduled_pause_finish_pattern,
         "finish_pattern": state.scheduled_pause_finish_pattern,
+        "timezone": state.scheduled_pause_timezone,
         "time_slots": state.scheduled_pause_time_slots
         "time_slots": state.scheduled_pause_time_slots
     }
     }
 
 
@@ -794,12 +805,19 @@ async def set_scheduled_pause(request: ScheduledPauseRequest):
         state.scheduled_pause_enabled = request.enabled
         state.scheduled_pause_enabled = request.enabled
         state.scheduled_pause_control_wled = request.control_wled
         state.scheduled_pause_control_wled = request.control_wled
         state.scheduled_pause_finish_pattern = request.finish_pattern
         state.scheduled_pause_finish_pattern = request.finish_pattern
+        state.scheduled_pause_timezone = request.timezone if request.timezone else None
         state.scheduled_pause_time_slots = [slot.model_dump() for slot in request.time_slots]
         state.scheduled_pause_time_slots = [slot.model_dump() for slot in request.time_slots]
         state.save()
         state.save()
 
 
+        # Clear cached timezone so it picks up the new setting
+        from modules.core import pattern_manager
+        pattern_manager._cached_timezone = None
+        pattern_manager._cached_zoneinfo = None
+
         wled_msg = " (with WLED control)" if request.control_wled else ""
         wled_msg = " (with WLED control)" if request.control_wled else ""
         finish_msg = " (finish pattern first)" if request.finish_pattern else ""
         finish_msg = " (finish pattern first)" if request.finish_pattern else ""
-        logger.info(f"Still Sands {'enabled' if request.enabled else 'disabled'} with {len(request.time_slots)} time slots{wled_msg}{finish_msg}")
+        tz_msg = f" (timezone: {request.timezone})" if request.timezone else ""
+        logger.info(f"Still Sands {'enabled' if request.enabled else 'disabled'} with {len(request.time_slots)} time slots{wled_msg}{finish_msg}{tz_msg}")
         return {"success": True, "message": "Still Sands settings updated"}
         return {"success": True, "message": "Still Sands settings updated"}
 
 
     except HTTPException:
     except HTTPException:

+ 29 - 24
modules/core/pattern_manager.py

@@ -35,12 +35,12 @@ pattern_lock = asyncio.Lock()
 # Progress update task
 # Progress update task
 progress_update_task = None
 progress_update_task = None
 
 
-# Cache timezone at module level - read once per session
+# Cache timezone at module level - read once per session (cleared when user changes timezone)
 _cached_timezone = None
 _cached_timezone = None
 _cached_zoneinfo = None
 _cached_zoneinfo = None
 
 
-def _get_system_timezone():
-    """Get and cache the system timezone. Called once per session."""
+def _get_timezone():
+    """Get and cache the timezone for Still Sands. Uses user-selected timezone if set, otherwise system timezone."""
     global _cached_timezone, _cached_zoneinfo
     global _cached_timezone, _cached_zoneinfo
 
 
     if _cached_timezone is not None:
     if _cached_timezone is not None:
@@ -48,25 +48,30 @@ def _get_system_timezone():
 
 
     user_tz = 'UTC'  # Default fallback
     user_tz = 'UTC'  # Default fallback
 
 
-    # Try to read timezone from /etc/host-timezone (mounted from host)
-    try:
-        if os.path.exists('/etc/host-timezone'):
-            with open('/etc/host-timezone', 'r') as f:
-                user_tz = f.read().strip()
-                logger.info(f"Still Sands using timezone: {user_tz} (from host system)")
-        # Fallback to /etc/timezone if host-timezone doesn't exist
-        elif os.path.exists('/etc/timezone'):
-            with open('/etc/timezone', 'r') as f:
-                user_tz = f.read().strip()
-                logger.info(f"Still Sands using timezone: {user_tz} (from container)")
-        # Fallback to TZ environment variable
-        elif os.environ.get('TZ'):
-            user_tz = os.environ.get('TZ')
-            logger.info(f"Still Sands using timezone: {user_tz} (from environment)")
-        else:
-            logger.info("Still Sands using timezone: UTC (default)")
-    except Exception as e:
-        logger.debug(f"Could not read timezone: {e}")
+    # First, check if user has selected a specific timezone in settings
+    if state.scheduled_pause_timezone:
+        user_tz = state.scheduled_pause_timezone
+        logger.info(f"Still Sands using timezone: {user_tz} (user-selected)")
+    else:
+        # Fall back to system timezone detection
+        try:
+            if os.path.exists('/etc/host-timezone'):
+                with open('/etc/host-timezone', 'r') as f:
+                    user_tz = f.read().strip()
+                    logger.info(f"Still Sands using timezone: {user_tz} (from host system)")
+            # Fallback to /etc/timezone if host-timezone doesn't exist
+            elif os.path.exists('/etc/timezone'):
+                with open('/etc/timezone', 'r') as f:
+                    user_tz = f.read().strip()
+                    logger.info(f"Still Sands using timezone: {user_tz} (from container)")
+            # Fallback to TZ environment variable
+            elif os.environ.get('TZ'):
+                user_tz = os.environ.get('TZ')
+                logger.info(f"Still Sands using timezone: {user_tz} (from environment)")
+            else:
+                logger.info("Still Sands using timezone: UTC (system default)")
+        except Exception as e:
+            logger.debug(f"Could not read timezone: {e}")
 
 
     # Cache the timezone
     # Cache the timezone
     _cached_timezone = user_tz
     _cached_timezone = user_tz
@@ -83,8 +88,8 @@ def is_in_scheduled_pause_period():
     if not state.scheduled_pause_enabled or not state.scheduled_pause_time_slots:
     if not state.scheduled_pause_enabled or not state.scheduled_pause_time_slots:
         return False
         return False
 
 
-    # Get cached timezone
-    tz_info = _get_system_timezone()
+    # Get cached timezone (user-selected or system default)
+    tz_info = _get_timezone()
 
 
     try:
     try:
         # Get current time in user's timezone
         # Get current time in user's timezone

+ 3 - 0
modules/core/state.py

@@ -103,6 +103,7 @@ class AppState:
         self.scheduled_pause_time_slots = []  # List of time slot dictionaries
         self.scheduled_pause_time_slots = []  # List of time slot dictionaries
         self.scheduled_pause_control_wled = False  # Turn off WLED during pause periods
         self.scheduled_pause_control_wled = False  # Turn off WLED during pause periods
         self.scheduled_pause_finish_pattern = False  # Finish current pattern before pausing
         self.scheduled_pause_finish_pattern = False  # Finish current pattern before pausing
+        self.scheduled_pause_timezone = None  # User-selected timezone (None = use system timezone)
 
 
         # MQTT settings (UI-configurable, overrides .env if set)
         # MQTT settings (UI-configurable, overrides .env if set)
         self.mqtt_enabled = False  # Master enable/disable for MQTT
         self.mqtt_enabled = False  # Master enable/disable for MQTT
@@ -265,6 +266,7 @@ class AppState:
             "scheduled_pause_time_slots": self.scheduled_pause_time_slots,
             "scheduled_pause_time_slots": self.scheduled_pause_time_slots,
             "scheduled_pause_control_wled": self.scheduled_pause_control_wled,
             "scheduled_pause_control_wled": self.scheduled_pause_control_wled,
             "scheduled_pause_finish_pattern": self.scheduled_pause_finish_pattern,
             "scheduled_pause_finish_pattern": self.scheduled_pause_finish_pattern,
+            "scheduled_pause_timezone": self.scheduled_pause_timezone,
             "mqtt_enabled": self.mqtt_enabled,
             "mqtt_enabled": self.mqtt_enabled,
             "mqtt_broker": self.mqtt_broker,
             "mqtt_broker": self.mqtt_broker,
             "mqtt_port": self.mqtt_port,
             "mqtt_port": self.mqtt_port,
@@ -347,6 +349,7 @@ class AppState:
         self.scheduled_pause_time_slots = data.get("scheduled_pause_time_slots", [])
         self.scheduled_pause_time_slots = data.get("scheduled_pause_time_slots", [])
         self.scheduled_pause_control_wled = data.get("scheduled_pause_control_wled", False)
         self.scheduled_pause_control_wled = data.get("scheduled_pause_control_wled", False)
         self.scheduled_pause_finish_pattern = data.get("scheduled_pause_finish_pattern", False)
         self.scheduled_pause_finish_pattern = data.get("scheduled_pause_finish_pattern", False)
+        self.scheduled_pause_timezone = data.get("scheduled_pause_timezone", None)
         self.mqtt_enabled = data.get("mqtt_enabled", False)
         self.mqtt_enabled = data.get("mqtt_enabled", False)
         self.mqtt_broker = data.get("mqtt_broker", "")
         self.mqtt_broker = data.get("mqtt_broker", "")
         self.mqtt_port = data.get("mqtt_port", 1883)
         self.mqtt_port = data.get("mqtt_port", 1883)

+ 52 - 12
static/js/settings.js

@@ -1286,8 +1286,18 @@ async function initializeauto_playMode() {
         logMessage(`Error loading auto_play settings: ${error.message}`, LOG_TYPE.ERROR);
         logMessage(`Error loading auto_play settings: ${error.message}`, LOG_TYPE.ERROR);
     }
     }
     
     
+    // Get save button
+    const saveAutoPlayButton = document.getElementById('saveAutoPlaySettings');
+
     // Function to save settings
     // Function to save settings
-    async function saveSettings() {
+    async function saveSettings(showFeedback = false) {
+        const originalButtonHTML = saveAutoPlayButton ? saveAutoPlayButton.innerHTML : '';
+
+        if (showFeedback && saveAutoPlayButton) {
+            saveAutoPlayButton.disabled = true;
+            saveAutoPlayButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Saving...</span>';
+        }
+
         try {
         try {
             const response = await fetch('/api/auto_play-mode', {
             const response = await fetch('/api/auto_play-mode', {
                 method: 'POST',
                 method: 'POST',
@@ -1301,28 +1311,42 @@ async function initializeauto_playMode() {
                     shuffle: auto_playShuffleToggle.checked
                     shuffle: auto_playShuffleToggle.checked
                 })
                 })
             });
             });
-            
+
             if (!response.ok) {
             if (!response.ok) {
                 throw new Error('Failed to save settings');
                 throw new Error('Failed to save settings');
             }
             }
+
+            if (showFeedback && saveAutoPlayButton) {
+                saveAutoPlayButton.innerHTML = '<span class="material-icons text-lg">check</span><span class="truncate">Saved!</span>';
+                showStatusMessage('Auto-play settings saved successfully', 'success');
+
+                setTimeout(() => {
+                    saveAutoPlayButton.innerHTML = originalButtonHTML;
+                    saveAutoPlayButton.disabled = false;
+                }, 2000);
+            }
         } catch (error) {
         } catch (error) {
             logMessage(`Error saving auto_play settings: ${error.message}`, LOG_TYPE.ERROR);
             logMessage(`Error saving auto_play settings: ${error.message}`, LOG_TYPE.ERROR);
+            if (showFeedback && saveAutoPlayButton) {
+                showStatusMessage(`Failed to save settings: ${error.message}`, 'error');
+                saveAutoPlayButton.innerHTML = originalButtonHTML;
+                saveAutoPlayButton.disabled = false;
+            }
         }
         }
     }
     }
-    
+
     // Toggle auto_play settings visibility and save
     // Toggle auto_play settings visibility and save
     auto_playToggle.addEventListener('change', async () => {
     auto_playToggle.addEventListener('change', async () => {
         auto_playSettings.style.display = auto_playToggle.checked ? 'block' : 'none';
         auto_playSettings.style.display = auto_playToggle.checked ? 'block' : 'none';
-        await saveSettings();
+        await saveSettings(false); // Auto-save toggle state without full feedback
+        const statusText = auto_playToggle.checked ? 'enabled' : 'disabled';
+        showStatusMessage(`Auto-play ${statusText}`, 'success');
     });
     });
-    
-    // Save when any setting changes
-    auto_playPlaylistSelect.addEventListener('change', saveSettings);
-    auto_playRunModeSelect.addEventListener('change', saveSettings);
-    auto_playPauseTimeInput.addEventListener('change', saveSettings);
-    auto_playPauseTimeInput.addEventListener('input', saveSettings); // Save as user types
-    auto_playClearPatternSelect.addEventListener('change', saveSettings);
-    auto_playShuffleToggle.addEventListener('change', saveSettings);
+
+    // Save button click handler
+    if (saveAutoPlayButton) {
+        saveAutoPlayButton.addEventListener('click', () => saveSettings(true));
+    }
 }
 }
 
 
 // Initialize auto_play mode when DOM is ready
 // Initialize auto_play mode when DOM is ready
@@ -1343,6 +1367,7 @@ async function initializeStillSandsMode() {
     const timeSlotsContainer = document.getElementById('timeSlotsContainer');
     const timeSlotsContainer = document.getElementById('timeSlotsContainer');
     const wledControlToggle = document.getElementById('stillSandsWledControl');
     const wledControlToggle = document.getElementById('stillSandsWledControl');
     const finishPatternToggle = document.getElementById('stillSandsFinishPattern');
     const finishPatternToggle = document.getElementById('stillSandsFinishPattern');
+    const timezoneSelect = document.getElementById('stillSandsTimezone');
 
 
     // Check if elements exist
     // Check if elements exist
     if (!stillSandsToggle || !stillSandsSettings || !addTimeSlotButton || !saveStillSandsButton || !timeSlotsContainer) {
     if (!stillSandsToggle || !stillSandsSettings || !addTimeSlotButton || !saveStillSandsButton || !timeSlotsContainer) {
@@ -1386,6 +1411,11 @@ async function initializeStillSandsMode() {
             finishPatternToggle.checked = data.finish_pattern || false;
             finishPatternToggle.checked = data.finish_pattern || false;
         }
         }
 
 
+        // Load timezone setting
+        if (timezoneSelect) {
+            timezoneSelect.value = data.timezone || '';
+        }
+
         // Load existing time slots
         // Load existing time slots
         timeSlots = data.time_slots || [];
         timeSlots = data.time_slots || [];
 
 
@@ -1611,6 +1641,7 @@ async function initializeStillSandsMode() {
                     enabled: stillSandsToggle.checked,
                     enabled: stillSandsToggle.checked,
                     control_wled: wledControlToggle ? wledControlToggle.checked : false,
                     control_wled: wledControlToggle ? wledControlToggle.checked : false,
                     finish_pattern: finishPatternToggle ? finishPatternToggle.checked : false,
                     finish_pattern: finishPatternToggle ? finishPatternToggle.checked : false,
+                    timezone: timezoneSelect ? (timezoneSelect.value || null) : null,
                     time_slots: timeSlots.map(slot => ({
                     time_slots: timeSlots.map(slot => ({
                         start_time: slot.start_time,
                         start_time: slot.start_time,
                         end_time: slot.end_time,
                         end_time: slot.end_time,
@@ -1683,6 +1714,15 @@ async function initializeStillSandsMode() {
             await saveStillSandsSettings();
             await saveStillSandsSettings();
         });
         });
     }
     }
+
+    // Add listener for timezone select
+    if (timezoneSelect) {
+        timezoneSelect.addEventListener('change', async () => {
+            logMessage(`Timezone changed: ${timezoneSelect.value || 'System Default'}`, LOG_TYPE.INFO);
+            // Auto-save when timezone changes
+            await saveStillSandsSettings();
+        });
+    }
 }
 }
 
 
 // Homing Configuration
 // Homing Configuration

+ 135 - 5
templates/settings.html

@@ -33,7 +33,7 @@ endblock %}
   border-color: #404040;
   border-color: #404040;
 }
 }
 .dark .divide-slate-100 {
 .dark .divide-slate-100 {
-  border-color: #404040;
+  border-color: #333333;
 }
 }
 .dark .bg-slate-50 {
 .dark .bg-slate-50 {
   background-color: #262626;
   background-color: #262626;
@@ -258,6 +258,36 @@ input:checked + .slider:before {
 .dark .text-amber-600 {
 .dark .text-amber-600 {
   color: #f1f5f9;
   color: #f1f5f9;
 }
 }
+
+/* Sky box dark mode - grey theme (Still Sands options) */
+.dark .bg-sky-50 {
+  background-color: #1f1f1f;
+}
+
+.dark .border-sky-200 {
+  border-color: #404040;
+}
+
+/* Select dropdown dark mode */
+.dark select {
+  background-color: #1f1f1f;
+  border-color: #404040;
+  color: #e5e5e5;
+}
+
+.dark select:focus {
+  border-color: #0c7ff2;
+}
+
+.dark select option {
+  background-color: #1f1f1f;
+  color: #e5e5e5;
+}
+
+.dark select optgroup {
+  background-color: #262626;
+  color: #9ca3af;
+}
 {% endblock %}
 {% endblock %}
 
 
 {% block content %}
 {% block content %}
@@ -277,7 +307,7 @@ input:checked + .slider:before {
     >
     >
       Device Connection
       Device Connection
     </h2>
     </h2>
-    <div class="divide-y divide-slate-100">
+    <div>
       <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"
       >
       >
@@ -331,7 +361,7 @@ input:checked + .slider:before {
         </label>
         </label>
       </div>
       </div>
       <!-- Preferred Port Configuration -->
       <!-- Preferred Port Configuration -->
-      <div class="px-6 py-5 border-t border-slate-100">
+      <div class="px-6 py-5">
         <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 flex items-center gap-2">
           <span class="text-slate-700 text-sm font-medium leading-normal flex items-center gap-2">
             <span class="material-icons text-slate-600 text-base">star</span>
             <span class="material-icons text-slate-600 text-base">star</span>
@@ -1090,6 +1120,16 @@ input:checked + .slider:before {
             </label>
             </label>
           </div>
           </div>
         </div>
         </div>
+
+        <div class="flex justify-end">
+          <button
+            id="saveAutoPlaySettings"
+            class="flex items-center justify-center gap-2 min-w-[140px] 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"
+          >
+            <span class="material-icons text-lg">save</span>
+            <span class="truncate">Save Auto-play</span>
+          </button>
+        </div>
       </div>
       </div>
     </div>
     </div>
   </section>
   </section>
@@ -1152,6 +1192,96 @@ input:checked + .slider:before {
           </div>
           </div>
         </div>
         </div>
 
 
+        <!-- Timezone Selection -->
+        <div class="bg-sky-50 rounded-lg p-4 border border-sky-200">
+          <div class="flex items-center justify-between">
+            <div class="flex-1">
+              <h4 class="text-slate-800 text-sm font-medium flex items-center gap-2">
+                <span class="material-icons text-slate-800 dark:text-slate-200 text-base">schedule</span>
+                Timezone
+              </h4>
+              <p class="text-xs text-slate-600 mt-1">
+                Select a timezone for still periods (defaults to system timezone)
+              </p>
+            </div>
+            <select id="stillSandsTimezone" class="h-10 px-3 rounded-lg border border-slate-300 bg-white text-slate-800 text-sm min-w-[200px]">
+              <option value="">System Default</option>
+              <optgroup label="Americas">
+                <option value="America/New_York">Eastern Time (New York)</option>
+                <option value="America/Chicago">Central Time (Chicago)</option>
+                <option value="America/Denver">Mountain Time (Denver)</option>
+                <option value="America/Los_Angeles">Pacific Time (Los Angeles)</option>
+                <option value="America/Anchorage">Alaska (Anchorage)</option>
+                <option value="Pacific/Honolulu">Hawaii (Honolulu)</option>
+                <option value="America/Toronto">Toronto</option>
+                <option value="America/Vancouver">Vancouver</option>
+                <option value="America/Mexico_City">Mexico City</option>
+                <option value="America/Sao_Paulo">São Paulo</option>
+                <option value="America/Buenos_Aires">Buenos Aires</option>
+              </optgroup>
+              <optgroup label="Europe">
+                <option value="Europe/London">London</option>
+                <option value="Europe/Paris">Paris</option>
+                <option value="Europe/Berlin">Berlin</option>
+                <option value="Europe/Amsterdam">Amsterdam</option>
+                <option value="Europe/Rome">Rome</option>
+                <option value="Europe/Madrid">Madrid</option>
+                <option value="Europe/Zurich">Zurich</option>
+                <option value="Europe/Stockholm">Stockholm</option>
+                <option value="Europe/Moscow">Moscow</option>
+              </optgroup>
+              <optgroup label="Asia & Pacific">
+                <option value="Asia/Tokyo">Tokyo</option>
+                <option value="Asia/Shanghai">Shanghai</option>
+                <option value="Asia/Hong_Kong">Hong Kong</option>
+                <option value="Asia/Singapore">Singapore</option>
+                <option value="Asia/Seoul">Seoul</option>
+                <option value="Asia/Dubai">Dubai</option>
+                <option value="Asia/Kolkata">India (Kolkata)</option>
+                <option value="Asia/Bangkok">Bangkok</option>
+                <option value="Australia/Sydney">Sydney</option>
+                <option value="Australia/Melbourne">Melbourne</option>
+                <option value="Australia/Perth">Perth</option>
+                <option value="Pacific/Auckland">Auckland</option>
+              </optgroup>
+              <optgroup label="Africa">
+                <option value="Africa/Cairo">Cairo</option>
+                <option value="Africa/Johannesburg">Johannesburg</option>
+                <option value="Africa/Lagos">Lagos</option>
+              </optgroup>
+              <optgroup label="GMT Offsets">
+                <option value="Etc/GMT+12">GMT-12</option>
+                <option value="Etc/GMT+11">GMT-11</option>
+                <option value="Etc/GMT+10">GMT-10</option>
+                <option value="Etc/GMT+9">GMT-9</option>
+                <option value="Etc/GMT+8">GMT-8</option>
+                <option value="Etc/GMT+7">GMT-7</option>
+                <option value="Etc/GMT+6">GMT-6</option>
+                <option value="Etc/GMT+5">GMT-5</option>
+                <option value="Etc/GMT+4">GMT-4</option>
+                <option value="Etc/GMT+3">GMT-3</option>
+                <option value="Etc/GMT+2">GMT-2</option>
+                <option value="Etc/GMT+1">GMT-1</option>
+                <option value="Etc/GMT">GMT / UTC</option>
+                <option value="Etc/GMT-1">GMT+1</option>
+                <option value="Etc/GMT-2">GMT+2</option>
+                <option value="Etc/GMT-3">GMT+3</option>
+                <option value="Etc/GMT-4">GMT+4</option>
+                <option value="Etc/GMT-5">GMT+5</option>
+                <option value="Etc/GMT-6">GMT+6</option>
+                <option value="Etc/GMT-7">GMT+7</option>
+                <option value="Etc/GMT-8">GMT+8</option>
+                <option value="Etc/GMT-9">GMT+9</option>
+                <option value="Etc/GMT-10">GMT+10</option>
+                <option value="Etc/GMT-11">GMT+11</option>
+                <option value="Etc/GMT-12">GMT+12</option>
+                <option value="Etc/GMT-13">GMT+13</option>
+                <option value="Etc/GMT-14">GMT+14</option>
+              </optgroup>
+            </select>
+          </div>
+        </div>
+
         <div class="bg-slate-50 rounded-lg p-4 space-y-4">
         <div class="bg-slate-50 rounded-lg p-4 space-y-4">
           <div class="flex items-center justify-between">
           <div class="flex items-center justify-between">
             <h4 class="text-slate-800 text-base font-semibold">Still Periods</h4>
             <h4 class="text-slate-800 text-base font-semibold">Still Periods</h4>
@@ -1177,7 +1307,7 @@ input:checked + .slider:before {
               <div>
               <div>
                 <p class="font-medium text-blue-800">Important Notes:</p>
                 <p class="font-medium text-blue-800">Important Notes:</p>
                 <ul class="mt-1 space-y-1 text-blue-700">
                 <ul class="mt-1 space-y-1 text-blue-700">
-                  <li>• Times are based on your system's local time zone</li>
+                  <li>• Times are based on the selected timezone (or system default if not set)</li>
                   <li>• By default, patterns pause immediately when entering a still period</li>
                   <li>• By default, patterns pause immediately when entering a still period</li>
                   <li>• Enable "Finish Current Pattern" to let patterns complete first</li>
                   <li>• Enable "Finish Current Pattern" to let patterns complete first</li>
                   <li>• Patterns will resume automatically when exiting a still period</li>
                   <li>• Patterns will resume automatically when exiting a still period</li>
@@ -1206,7 +1336,7 @@ input:checked + .slider:before {
     >
     >
       Software Version
       Software Version
     </h2>
     </h2>
-    <div class="divide-y divide-slate-100">
+    <div>
       <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"