Kaynağa Gözat

Remove old Jinja frontend code and assets

This cleanup removes the legacy Jinja-based frontend now that the
React frontend is the primary UI.

Removed:
- templates/*.html (except redirect.html for port 8080 users)
- static/js/*.js (old frontend JavaScript)
- static/css/*.css (old Tailwind and vendor CSS)
- static/fonts/, static/webfonts/, static/icons/ (old font files)
- static/IMG_*.png, static/UI*.png (old screenshots)
- tailwind.config.js (legacy Tailwind config)
- Legacy npm scripts in package.json (build-css, watch-css)
- Unused devDependencies (tailwindcss, vite, typescript at root)

Kept:
- static/custom/ (user branding uploads)
- static/dist/ (React build output)
- static/favicon*.png, apple-touch-icon.png (PWA assets)
- static/site.webmanifest (PWA manifest)
- templates/redirect.html (redirects port 8080 users to port 80)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 3 hafta önce
ebeveyn
işleme
2d833dd71f
44 değiştirilmiş dosya ile 0 ekleme ve 16692 silme
  1. BIN
      static/IMG_7404.gif
  2. BIN
      static/IMG_9753.png
  3. BIN
      static/UI.png
  4. BIN
      static/UI_1.3.png
  5. 0 5
      static/css/all.min.css
  6. 0 0
      static/css/coloris.min.css
  7. 0 323
      static/css/image2sand.css
  8. 0 53
      static/css/material-icons.css
  9. 0 195
      static/css/tailwind-input.css
  10. 0 0
      static/css/tailwind.css
  11. 0 5
      static/fontawesome.min.css
  12. BIN
      static/fonts/material-icons/MaterialIcons-Regular.woff2
  13. BIN
      static/fonts/material-icons/MaterialIconsOutlined-Regular.woff2
  14. 0 1
      static/icons/chevron-down.svg
  15. 0 1
      static/icons/chevron-left.svg
  16. 0 1
      static/icons/chevron-right.svg
  17. 0 1
      static/icons/chevron-up.svg
  18. 0 1
      static/icons/pause.svg
  19. 0 1
      static/icons/play.svg
  20. 0 1213
      static/js/base.js
  21. 0 5
      static/js/coloris.min.js
  22. 0 405
      static/js/image2sand-init.js
  23. 0 1400
      static/js/image2sand.js
  24. 0 2451
      static/js/index.js
  25. 0 828
      static/js/led-control.js
  26. 0 29
      static/js/opencv.js
  27. 0 1935
      static/js/playlists.js
  28. 0 2439
      static/js/settings.js
  29. 0 209
      static/js/table_control.js
  30. BIN
      static/webfonts/Roboto-Italic-VariableFont_wdth,wght.ttf
  31. BIN
      static/webfonts/Roboto-VariableFont_wdth,wght.ttf
  32. BIN
      static/webfonts/fa-regular-400.ttf
  33. BIN
      static/webfonts/fa-regular-400.woff2
  34. BIN
      static/webfonts/fa-solid-900.ttf
  35. BIN
      static/webfonts/fa-solid-900.woff2
  36. 0 19
      tailwind.config.js
  37. 0 1196
      templates/base.html
  38. 0 178
      templates/design.html
  39. 0 408
      templates/image2sand.html
  40. 0 654
      templates/index.html
  41. 0 387
      templates/led.html
  42. 0 443
      templates/playlists.html
  43. 0 1552
      templates/settings.html
  44. 0 354
      templates/table_control.html

BIN
static/IMG_7404.gif


BIN
static/IMG_9753.png


BIN
static/UI.png


BIN
static/UI_1.3.png


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 5
static/css/all.min.css


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/css/coloris.min.css


+ 0 - 323
static/css/image2sand.css

@@ -1,323 +0,0 @@
-/* Image Converter Section Styles */
-.image-converter-content {
-    overflow-y: auto;
-    display: flex;
-    flex-direction: column;
-    gap: 20px;
-    flex-grow: 1;
-}
-
-.image-converter-steps .tab-container {
-    display: flex;
-    width: 100%;
-}
-
-.image-converter-steps .tab-button.active {
-    background: rgba(255, 255, 255, 0.75);
-    color: var(--theme-primary);
-}
-
-.image-converter-steps canvas {
-    background-color: white;
-}
-
-.image-converter-steps .tab-content.active {
-    flex: auto;
-}
-
-#generate-button.loading {
-    position: relative;
-    pointer-events: none;
-    opacity: 0.7;
-}
-
-#generate-button.loading i {
-    display: none;
-}
-
-#generate-button.loading::before {
-    content: "";
-    width: 16px;
-    height: 16px;
-    border: 3px solid white;
-    border-top-color: transparent;
-    border-radius: 50%;
-    animation: spin 1s linear infinite;
-}
-
-@keyframes spin {
-    from { transform: rotate(0deg); }
-    to { transform: rotate(360deg); }
-}
-
-.image-converter-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    margin-bottom: 20px;
-    border-bottom: 1px solid var(--border-primary);
-    padding-bottom: 10px;
-}
-
-.image-converter-header h2 {
-    margin: 0;
-    color: var(--theme-secondary-hover);
-}
-
-.preview-controls-container {
-    display: grid;
-    grid-template-columns: minmax(300px, 1fr) minmax(300px, 1fr);
-    gap: 20px;
-    margin-bottom: 20px;
-}
-
-.image-converter-settings {
-    display: flex;
-    flex-direction: column;
-    gap: 15px;
-    padding: 15px;
-}
-
-.image-converter-preview,
-.dune-weaver-preview {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    margin-bottom: 15px;
-    flex-grow: 1;
-}
-
-.image-converter-preview canvas,
-.dune-weaver-preview canvas {
-    max-width: 100%;
-    border: 1px solid var(--border-primary);
-    background-color: #FFFFFF;
-    margin-bottom: 10px;
-    width: 100%;
-    height: auto;
-    aspect-ratio: 1 / 1;
-    object-fit: contain;
-    min-height: 250px;
-}
-
-/* Ensure all canvases have the same size */
-#original-image, #edge-image, #dot-image, #connect-image {
-    width: 100%;
-    height: auto;
-    aspect-ratio: 1 / 1;
-    min-height: 250px;
-}
-
-#connect-image {
-    /*border: 1px solid var(--border-primary);*/
-    /*background: var(--theme-secondary);*/
-    padding: 15px;
-}
-
-.dune-weaver-preview canvas {
-    min-height: 250px;
-}
-
-.image-converter-preview h3,
-.dune-weaver-preview h3 {
-    margin-top: 0;
-    margin-bottom: 8px;
-    font-size: 1rem;
-    color: var(--theme-secondary-hover);
-}
-
-.image-converter-actions {
-    display: flex;
-    justify-content: flex-end;
-    gap: 15px;
-    margin-top: 20px;
-    padding-top: 15px;
-    border-top: 1px solid var(--border-primary);
-}
-
-.image-converter-actions button {
-    padding: 10px 20px;
-    border-radius: 4px;
-    cursor: pointer;
-    font-weight: 500;
-    display: flex;
-    align-items: center;
-    gap: 8px;
-}
-
-.image-converter-actions button i {
-    font-size: 1.1rem;
-}
-
-.image-converter-actions .cancel {
-    background-color: var(--color-error);
-    color: var(--text-secondary);
-    border: none;
-}
-
-.image-converter-actions .cta {
-    background-color: var(--theme-primary);
-    color: var(--text-secondary);
-    border: none;
-}
-
-.slider-container {
-    display: flex;
-    flex-direction: column;
-    width: 100%;
-}
-
-.slider-labels {
-    display: flex;
-    justify-content: space-between;
-    font-size: 0.8rem;
-    color: var(--text-primary);
-}
-
-.setting-item {
-    display: flex;
-    flex-direction: column;
-}
-
-.setting-item label {
-    margin-bottom: 5px;
-    font-weight: 500;
-    color: var(--text-primary);
-}
-
-.setting-item select,
-.setting-item input {
-    padding: 8px;
-    border-radius: 4px;
-    border: 1px solid var(--border-primary);
-    background-color: var(--input-background);
-    color: var(--input-text);
-}
-
-.checkbox-group {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 20px;
-    margin: 10px 0;
-}
-
-.checkbox-container {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    white-space: nowrap;
-}
-
-.checkbox-container label {
-    margin-bottom: 0;
-    color: var(--text-primary);
-}
-
-.generate-button-container {
-    display: flex;
-    justify-content: center;
-    margin-top: 15px;
-}
-
-.generate-button-container button {
-    padding: 10px 20px;
-    font-size: 1rem;
-    background-color: var(--theme-primary);
-    color: var(--text-secondary);
-    border: none;
-    border-radius: 4px;
-    cursor: pointer;
-    display: flex;
-    align-items: center;
-    gap: 8px;
-}
-
-.generate-button-container button:hover {
-    background-color: var(--theme-primary-hover);
-}
-
-.processing-indicator {
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    background-color: var(--background-translucent);
-    display: flex;
-    flex-direction: column;
-    justify-content: center;
-    align-items: center;
-    z-index: 10;
-    color: var(--text-secondary);
-    font-size: 1.2rem;
-    visibility: hidden;
-    opacity: 0;
-    transition: opacity 0.3s, visibility 0.3s;
-}
-
-.processing-indicator.visible {
-    visibility: visible;
-    opacity: 1;
-}
-
-.spinner {
-    border: 4px solid rgba(255, 255, 255, 0.3);
-    border-radius: 50%;
-    border-top: 4px solid var(--theme-primary);
-    width: 40px;
-    height: 40px;
-    animation: spin 1s linear infinite;
-    margin-bottom: 15px;
-}
-
-@keyframes spin {
-    0% {
-        transform: rotate(0deg);
-    }
-    100% {
-        transform: rotate(360deg);
-    }
-}
-
-#total-points {
-    margin-top: 10px;
-    font-size: 0.9rem;
-    color: var(--text-primary);
-}
-
-/* Ensure inputs and checkboxes match the theme */
-input[type="checkbox"] {
-    accent-color: var(--theme-primary);
-}
-
-input[type="range"] {
-    accent-color: var(--theme-primary);
-    background-color: var(--input-background);
-}
-
-/* Ensure the dialog transitions smoothly with theme changes */
-.image-converter-dialog * {
-    transition: background-color var(--transition-fast),
-    color var(--transition-fast),
-    border-color var(--transition-fast);
-}
-
-.gen-button {
-    margin-left: 10px;
-}
-
-canvas#connect-image {
-    clip-path: ellipse();
-}
-
-
-@media (min-width: 769px) {
-    .image-converter-content {
-        flex-direction: row;
-    }
-
-    .image-converter-content > div {
-        width: calc(100% / 3);
-    }
-
-}

+ 0 - 53
static/css/material-icons.css

@@ -1,53 +0,0 @@
-/* Material Icons - Local Font Files */
-
-@font-face {
-  font-family: 'Material Icons';
-  font-style: normal;
-  font-weight: 400;
-  src: url('/static/fonts/material-icons/MaterialIcons-Regular.woff2') format('woff2');
-  font-display: block;
-}
-
-@font-face {
-  font-family: 'Material Icons Outlined';
-  font-style: normal;
-  font-weight: 400;
-  src: url('/static/fonts/material-icons/MaterialIconsOutlined-Regular.woff2') format('woff2');
-  font-display: block;
-}
-
-.material-icons {
-  font-family: 'Material Icons';
-  font-weight: normal;
-  font-style: normal;
-  font-size: 24px;
-  display: inline-block;
-  line-height: 1;
-  text-transform: none;
-  letter-spacing: normal;
-  word-wrap: normal;
-  white-space: nowrap;
-  direction: ltr;
-  -webkit-font-smoothing: antialiased;
-  text-rendering: optimizeLegibility;
-  -moz-osx-font-smoothing: grayscale;
-  font-feature-settings: "liga";
-}
-
-.material-icons-outlined {
-  font-family: 'Material Icons Outlined';
-  font-weight: normal;
-  font-style: normal;
-  font-size: 24px;
-  display: inline-block;
-  line-height: 1;
-  text-transform: none;
-  letter-spacing: normal;
-  word-wrap: normal;
-  white-space: nowrap;
-  direction: ltr;
-  -webkit-font-smoothing: antialiased;
-  text-rendering: optimizeLegibility;
-  -moz-osx-font-smoothing: grayscale;
-  font-feature-settings: "liga";
-}

+ 0 - 195
static/css/tailwind-input.css

@@ -1,195 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-/* Custom styles from base.html */
-
-/* Dark mode styles */
-.dark {
-  color-scheme: dark;
-}
-.dark body {
-  background-color: #1a1a1a;
-  color: #e5e5e5;
-}
-.dark header {
-  background-color: #262626;
-  border-color: #404040;
-}
-.dark footer {
-  background-color: #262626;
-  border-color: #404040;
-}
-.dark .inactive-tab {
-  color: #9ca3af;
-}
-.dark .inactive-tab:hover {
-  color: #d1d5db;
-}
-.dark #player-status-bar-container {
-  background-color: #262626;
-  color: #e5e5e5;
-}
-.dark .bg-gray-100 {
-  background-color: #262626;
-}
-.dark .bg-gray-200 {
-  background-color: #404040;
-}
-.dark .bg-gray-300 {
-  background-color: #525252;
-}
-.dark .text-gray-500 {
-  color: #9ca3af;
-}
-.dark .text-gray-700 {
-  color: #d1d5db;
-}
-.dark .text-gray-800 {
-  color: #e5e5e5;
-}
-.dark .border-gray-200 {
-  border-color: #404040;
-}
-.dark .hover\:bg-gray-200:hover {
-  background-color: #404040;
-}
-.dark .hover\:bg-gray-300:hover {
-  background-color: #525252;
-}
-.dark .hover\:border-gray-300:hover {
-  border-color: #525252;
-}
-.dark .hover\:bg-gray-50:hover {
-  background-color: #262626;
-}
-.dark .bg-white {
-  background-color: #262626;
-}
-.dark .bg-gray-50 {
-  background-color: #1a1a1a;
-}
-.dark .text-gray-900 {
-  color: #e5e5e5;
-}
-.dark .text-gray-400 {
-  color: #9ca3af;
-}
-.dark .border-gray-300 {
-  border-color: #404040;
-}
-.dark .focus\:ring-offset-2 {
-  --tw-ring-offset-color: #262626;
-}
-.dark .shadow-sm {
-  box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
-}
-.dark .shadow-lg {
-  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
-}
-.dark .shadow-xl {
-  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
-}
-
-/* Tab styles */
-@layer components {
-  .active-tab {
-    @apply text-blue-600 border-blue-600;
-  }
-  .active-tab .material-icons {
-    @apply text-blue-600;
-  }
-  .inactive-tab {
-    @apply text-gray-500;
-  }
-  .inactive-tab:hover {
-    @apply text-gray-700;
-  }
-  .btn {
-    @apply px-4 py-2 rounded-lg font-medium transition-colors duration-150;
-  }
-  .btn-primary {
-    @apply bg-blue-600 text-white hover:bg-blue-700;
-  }
-  .btn-secondary {
-    @apply bg-gray-200 text-gray-700 hover:bg-gray-300;
-  }
-  .form-input {
-    @apply block w-full rounded-lg border-gray-300 shadow-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500;
-  }
-}
-
-/* Status message transition */
-#status-message {
-  transition: opacity 0.5s ease-in-out, transform 0.5s ease-in-out;
-}
-
-/* Switch toggle styles */
-.switch {
-  position: relative;
-  display: inline-block;
-  width: 40px;
-  height: 20px;
-}
-
-.switch input {
-  opacity: 0;
-  width: 0;
-  height: 0;
-}
-
-.slider {
-  position: absolute;
-  cursor: pointer;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background-color: #ccc;
-  transition: .4s;
-  border-radius: 20px;
-}
-
-.slider:before {
-  position: absolute;
-  content: "";
-  height: 16px;
-  width: 16px;
-  left: 2px;
-  bottom: 2px;
-  background-color: white;
-  transition: .4s;
-  border-radius: 50%;
-}
-
-input:checked + .slider {
-  background-color: #0c7ff2;
-}
-
-input:checked + .slider:before {
-  transform: translateX(20px);
-}
-
-/* Shadow for top of status bar */
-.shadow-lg-top {
-  box-shadow: 0 -4px 6px -1px rgb(0 0 0 / 0.1), 0 -2px 4px -2px rgb(0 0 0 / 0.1);
-}
-
-/* Marquee animation for pattern name on small screens */
-@keyframes marquee {
-  0%   { transform: translateX(0%); }
-  50% { transform: translateX(-50%); }
-}
-
-.pattern-marquee {
-  display: inline-block;
-  min-width: 100%;
-  animation: marquee 8s linear infinite;
-}
-
-@media (min-width: 640px) { /* sm: and up, disable marquee */
-  .pattern-marquee {
-    animation: none !important;
-    transform: none !important;
-  }
-} 

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
static/css/tailwind.css


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 5
static/fontawesome.min.css


BIN
static/fonts/material-icons/MaterialIcons-Regular.woff2


BIN
static/fonts/material-icons/MaterialIconsOutlined-Regular.woff2


+ 0 - 1
static/icons/chevron-down.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"/></svg>

+ 0 - 1
static/icons/chevron-left.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M9.4 233.4c-12.5 12.5-12.5 32.8 0 45.3l192 192c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L77.3 256 246.6 86.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0l-192 192z"/></svg>

+ 0 - 1
static/icons/chevron-right.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M310.6 233.4c12.5 12.5 12.5 32.8 0 45.3l-192 192c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3L242.7 256 73.4 86.6c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0l192 192z"/></svg>

+ 0 - 1
static/icons/chevron-up.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M233.4 105.4c12.5-12.5 32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 173.3 86.6 342.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l192-192z"/></svg>

+ 0 - 1
static/icons/pause.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M48 64C21.5 64 0 85.5 0 112L0 400c0 26.5 21.5 48 48 48l32 0c26.5 0 48-21.5 48-48l0-288c0-26.5-21.5-48-48-48L48 64zm192 0c-26.5 0-48 21.5-48 48l0 288c0 26.5 21.5 48 48 48l32 0c26.5 0 48-21.5 48-48l0-288c0-26.5-21.5-48-48-48l-32 0z"/></svg>

+ 0 - 1
static/icons/play.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--! Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc. --><path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80L0 432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"/></svg>

+ 0 - 1213
static/js/base.js

@@ -1,1213 +0,0 @@
-// Player status bar functionality - Updated to fix logMessage errors
-
-// Update LED nav label based on provider
-async function updateLedNavLabel() {
-    try {
-        const response = await fetch('/get_led_config');
-        if (response.ok) {
-            const data = await response.json();
-            const navLabel = document.getElementById('led-nav-label');
-            if (navLabel) {
-                if (data.provider === 'wled') {
-                    navLabel.textContent = 'WLED';
-                } else if (data.provider === 'dw_leds') {
-                    navLabel.textContent = 'DW LEDs';
-                } else {
-                    navLabel.textContent = 'LED';
-                }
-            }
-        }
-    } catch (error) {
-        console.error('Error updating LED nav label:', error);
-    }
-}
-
-// Call on page load
-document.addEventListener('DOMContentLoaded', updateLedNavLabel);
-
-// Pattern files cache for improved performance with localStorage persistence
-const PATTERN_CACHE_KEY = 'dune_weaver_pattern_files_cache';
-const PATTERN_CACHE_EXPIRY = 30 * 60 * 1000; // 30 minutes cache (longer since it persists)
-
-// Function to get cached pattern files or fetch fresh data
-async function getCachedPatternFiles(forceRefresh = false) {
-    const now = Date.now();
-
-    // Try to load from localStorage first
-    if (!forceRefresh) {
-        try {
-            const cachedData = localStorage.getItem(PATTERN_CACHE_KEY);
-            if (cachedData) {
-                const { files, timestamp } = JSON.parse(cachedData);
-                if (files && timestamp && (now - timestamp) < PATTERN_CACHE_EXPIRY) {
-                    console.log('Using cached pattern files from localStorage');
-                    return files;
-                }
-            }
-        } catch (error) {
-            console.warn('Error reading pattern files cache from localStorage:', error);
-        }
-    }
-
-    try {
-        console.log('Fetching fresh pattern files from server');
-        const response = await fetch('/list_theta_rho_files');
-        if (!response.ok) {
-            throw new Error(`Failed to fetch pattern files: ${response.status}`);
-        }
-
-        const files = await response.json();
-
-        // Store in localStorage
-        try {
-            const cacheData = { files, timestamp: now };
-            localStorage.setItem(PATTERN_CACHE_KEY, JSON.stringify(cacheData));
-        } catch (error) {
-            console.warn('Error storing pattern files cache in localStorage:', error);
-        }
-
-        return files;
-    } catch (error) {
-        console.error('Error fetching pattern files:', error);
-
-        // Try to return any cached data as fallback, even if expired
-        try {
-            const cachedData = localStorage.getItem(PATTERN_CACHE_KEY);
-            if (cachedData) {
-                const { files } = JSON.parse(cachedData);
-                if (files) {
-                    console.log('Using expired cached pattern files as fallback');
-                    return files;
-                }
-            }
-        } catch (fallbackError) {
-            console.warn('Error reading fallback cache:', fallbackError);
-        }
-
-        return [];
-    }
-}
-
-// Function to invalidate pattern files cache
-function invalidatePatternFilesCache() {
-    try {
-        localStorage.removeItem(PATTERN_CACHE_KEY);
-        console.log('Pattern files cache invalidated');
-    } catch (error) {
-        console.warn('Error invalidating pattern files cache:', error);
-    }
-}
-
-// Helper function to normalize file paths for cross-platform compatibility
-function normalizeFilePath(filePath) {
-    if (!filePath) return '';
-    // First normalize path separators
-    let normalized = filePath.replace(/\\/g, '/');
-    
-    // Remove only the patterns directory prefix, not patterns within the path
-    if (normalized.startsWith('./patterns/')) {
-        normalized = normalized.substring(11);
-    } else if (normalized.startsWith('patterns/')) {
-        normalized = normalized.substring(9);
-    }
-    
-    return normalized;
-}
-
-let ws = null;
-let reconnectAttempts = 0;
-const maxReconnectAttempts = 5;
-const reconnectDelay = 3000; // 3 seconds
-let isEditingSpeed = false; // Track if user is editing speed
-
-// WebSocket UI update throttling for Pi performance
-let lastUIUpdate = 0;
-const UI_UPDATE_INTERVAL = 100; // Minimum ms between UI updates (10 updates/sec max)
-let playerPreviewData = null; // Store the current pattern's preview data for modal
-let playerPreviewCtx = null; // Store the canvas context for modal preview
-let playerAnimationId = null; // Store animation frame ID for modal
-let lastProgress = 0; // Last known progress from backend
-let targetProgress = 0; // Target progress to animate towards
-let animationStartTime = 0; // Start time of current animation
-let animationDuration = 1000; // Duration of interpolation in ms
-let smoothAnimationStartTime = 0; // Start time for smooth coordinate animation
-let smoothAnimationActive = false; // Whether smooth animation is running
-let modalAnimationId = null; // Store animation frame ID for modal
-let modalLastProgress = 0; // Last known progress for modal
-let modalTargetProgress = 0; // Target progress for modal
-let modalAnimationStartTime = 0; // Start time for modal animation
-let userDismissedModal = false; // Track if user has manually dismissed the modal
-
-// Function to set modal visibility
-function setModalVisibility(show, userAction = false) {
-    const modal = document.getElementById('playerPreviewModal');
-    if (!modal) return;
-    
-    if (show) {
-        modal.classList.remove('hidden');
-    } else {
-        modal.classList.add('hidden');
-    }
-    
-    if (userAction) {
-        userDismissedModal = !show;
-    }
-}
-let currentPreviewFile = null; // Track the current file for preview data
-
-// Global playback status for cross-file access
-window.currentPlaybackStatus = {
-    is_running: false,
-    current_file: null
-};
-
-function connectWebSocket() {
-    if (ws) {
-        ws.close();
-    }
-
-    ws = new WebSocket(`${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${window.location.host}/ws/status`);
-    
-    ws.onopen = function() {
-        console.log("WebSocket connection established");
-        reconnectAttempts = 0;
-    };
-
-    ws.onclose = function() {
-        console.log("WebSocket connection closed");
-        if (reconnectAttempts < maxReconnectAttempts) {
-            reconnectAttempts++;
-            setTimeout(connectWebSocket, reconnectDelay);
-        }
-    };
-
-    ws.onerror = function(error) {
-        console.error("WebSocket error:", error);
-    };
-
-    ws.onmessage = function(event) {
-        try {
-            const data = JSON.parse(event.data);
-            if (data.type === 'status_update') {
-                // Always update global playback status (not throttled)
-                // This ensures play button always has current state
-                window.currentPlaybackStatus = {
-                    is_running: data.data.is_running || false,
-                    current_file: data.data.current_file || null
-                };
-
-                // Throttle UI updates for better Pi performance
-                const now = Date.now();
-                if (now - lastUIUpdate < UI_UPDATE_INTERVAL) {
-                    return; // Skip this update, too soon
-                }
-                lastUIUpdate = now;
-
-                // Update modal status with the full data
-                syncModalControls(data.data);
-                
-                // Update speed input field on table control page if it exists
-                if (data.data && data.data.speed) {
-                    const currentSpeedDisplay = document.getElementById('currentSpeedDisplay');
-                    if (currentSpeedDisplay) {
-                        currentSpeedDisplay.textContent = `${data.data.speed} mm/s`;
-                    }
-                }
-                
-                // Update connection status dot using 'connection_status' or fallback to 'connected'
-                if (data.data.hasOwnProperty('connection_status')) {
-                    updateConnectionStatus(data.data.connection_status);
-                }
-                
-                // Check if current file has changed and reload preview data if needed
-                if (data.data.current_file) {
-                    const newFile = normalizeFilePath(data.data.current_file);
-                    if (newFile !== currentPreviewFile) {
-                        currentPreviewFile = newFile;
-
-                        // Only preload if we're on the browse page (index.html)
-                        // Other pages (playlists, table_control, LED, settings) will load on-demand
-                        const modal = document.getElementById('playerPreviewModal');
-                        const browsePage = document.getElementById('browseSortFieldSelect');
-
-                        if (modal && browsePage) {
-                            // We're on the browse page with the modal - preload coordinates
-                            loadPlayerPreviewData(data.data.current_file);
-                        }
-                    }
-                } else {
-                    currentPreviewFile = null;
-                    playerPreviewData = null;
-                }
-                
-                // Update progress for modal animation with smooth interpolation
-                if (playerPreviewData && data.data.progress && data.data.progress.percentage !== null) {
-                    const newProgress = data.data.progress.percentage / 100;
-                    targetProgress = newProgress;
-                    
-                    // Update modal if open with smooth animation
-                    const modal = document.getElementById('playerPreviewModal');
-                    if (modal && !modal.classList.contains('hidden')) {
-                        updateModalPreviewSmooth(newProgress);
-                    }
-                }
-                
-                // Reset userDismissedModal flag if no pattern is playing
-                if (!data.data.current_file || !data.data.is_running) {
-                    userDismissedModal = false;
-                }
-            }
-        } catch (error) {
-            console.error("Error processing WebSocket message:", error);
-        }
-    };
-}
-
-function updateConnectionStatus(isConnected) {
-    const statusDot = document.getElementById("connectionStatusDot");
-    if (statusDot) {
-        // Update dot color
-        statusDot.className = `inline-block size-2 rounded-full ml-2 align-middle ${
-            isConnected ? "bg-green-500" : "bg-red-500"
-        }`;
-    }
-}
-
-// Setup player preview with expand button
-function setupPlayerPreview() {
-    const previewContainer = document.getElementById('player-pattern-preview');
-    if (!previewContainer) return;
-
-    // Get current background image URL
-    const currentBgImage = previewContainer.style.backgroundImage;
-    
-    // Clear container
-    previewContainer.innerHTML = '';
-    previewContainer.style.backgroundImage = '';
-    
-    // Create preview image container
-    const imageContainer = document.createElement('div');
-    imageContainer.className = 'relative aspect-square rounded-full overflow-hidden w-full h-full';
-    
-    // Create image element
-    const img = document.createElement('img');
-    img.className = 'w-full h-full object-cover';
-    // img.alt = 'Pattern Preview';
-    // Extract URL from background-image CSS
-    img.src = currentBgImage.replace(/^url\(['"](.+)['"]\)$/, '$1');
-    
-    // Add expand button overlay
-    const expandOverlay = document.createElement('div');
-    expandOverlay.className = 'absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity duration-200 cursor-pointer z-20 bg-black bg-opacity-20 hover:bg-opacity-30';
-    expandOverlay.innerHTML = '<div class="bg-white rounded-full p-3 shadow-lg flex items-center justify-center w-12 h-12"><span class="material-icons text-xl text-gray-800">fullscreen</span></div>';
-    
-    // Add click handler for expand button
-    expandOverlay.addEventListener('click', (e) => {
-        e.stopPropagation();
-        openPlayerPreviewModal();
-    });
-    
-    // Add image and overlay to image container
-    imageContainer.appendChild(img);
-    imageContainer.appendChild(expandOverlay);
-    
-    // Add image container to preview container
-    previewContainer.appendChild(imageContainer);
-}
-
-// Open player preview modal
-async function openPlayerPreviewModal() {
-    try {
-        const modal = document.getElementById('playerPreviewModal');
-        const title = document.getElementById('playerPreviewTitle');
-        const canvas = document.getElementById('playerPreviewCanvas');
-        const ctx = canvas.getContext('2d');
-        const toggleBtn = document.getElementById('toggle-preview-modal-btn');
-
-        // Show modal immediately for instant feedback
-        modal.classList.remove('hidden');
-
-        // Setup canvas (so it's ready to display loading state)
-        setupPlayerPreviewCanvas(ctx);
-
-        // Load preview data on-demand if not already loaded
-        if (!playerPreviewData && currentPreviewFile) {
-            // Show loading state
-            title.textContent = 'Loading pattern...';
-            drawLoadingState(ctx);
-
-            // Load data in background
-            await loadPlayerPreviewData(`./patterns/${currentPreviewFile}`);
-
-            // Update title when loaded
-            title.textContent = 'Live Pattern Preview';
-        } else {
-            // Data already loaded
-            title.textContent = 'Live Pattern Preview';
-        }
-
-        // Draw the pattern (either immediately if cached, or after loading)
-        drawPlayerPreview(ctx, targetProgress);
-
-    } catch (error) {
-        console.error(`Error opening player preview modal: ${error.message}`);
-        showStatusMessage('Failed to load pattern for animation', 'error');
-    }
-}
-
-// Setup player preview canvas for modal
-function setupPlayerPreviewCanvas(ctx) {
-    const canvas = ctx.canvas;
-    const container = canvas.parentElement; // This is the div with max-w and max-h constraints
-    const modal = document.getElementById('playerPreviewModal');
-    
-    if (!container || !modal) return;
-    
-    // Calculate available viewport space directly
-    const viewportWidth = window.innerWidth;
-    const viewportHeight = window.innerHeight;
-    
-    // Calculate maximum canvas size based on viewport and fixed estimates
-    // Modal uses max-w-5xl (1024px) but we want to be responsive to actual viewport
-    const modalMaxWidth = Math.min(1024, viewportWidth * 0.9); // Account for modal padding
-    const modalMaxHeight = viewportHeight * 0.95; // max-h-[95vh]
-    
-    // Reserve space for modal header (~80px) and controls (~200px) and padding
-    const reservedSpace = 320; // Header + controls + padding
-    const availableModalHeight = modalMaxHeight - reservedSpace;
-    
-    // Calculate canvas constraints (stay within original 800px max, but be responsive)
-    const maxCanvasSize = Math.min(800, modalMaxWidth - 64, availableModalHeight); // 64px for canvas area padding
-    
-    // Ensure minimum size
-    const finalSize = Math.max(200, maxCanvasSize);
-    
-    // Update container to exact size (override CSS constraints)
-    container.style.width = `${finalSize}px`;
-    container.style.height = `${finalSize}px`;
-    container.style.maxWidth = `${finalSize}px`;
-    container.style.maxHeight = `${finalSize}px`;
-    container.style.minWidth = `${finalSize}px`;
-    container.style.minHeight = `${finalSize}px`;
-    
-    // Set the internal canvas size for high-DPI rendering
-    // Cap at 1.5x for better Pi performance (was 2x forced)
-    const pixelRatio = Math.min(window.devicePixelRatio || 1, 1.5);
-    canvas.width = finalSize * pixelRatio;
-    canvas.height = finalSize * pixelRatio;
-    
-    // Set the display size (canvas fills its container)
-    canvas.style.width = '100%';
-    canvas.style.height = '100%';
-    
-    console.log('Canvas resized:', {
-        viewport: `${viewportWidth}x${viewportHeight}`,
-        modalMaxWidth,
-        availableModalHeight,
-        finalSize: finalSize
-    });
-}
-
-// Get interpolated coordinate at specific progress
-function getInterpolatedCoordinate(progress) {
-    if (!playerPreviewData || playerPreviewData.length === 0) return null;
-    
-    const totalPoints = playerPreviewData.length;
-    const exactIndex = progress * (totalPoints - 1);
-    const index = Math.floor(exactIndex);
-    const fraction = exactIndex - index;
-    
-    // Ensure we don't go out of bounds
-    if (index >= totalPoints - 1) {
-        return playerPreviewData[totalPoints - 1];
-    }
-    
-    if (index < 0) {
-        return playerPreviewData[0];
-    }
-    
-    // Get the two coordinates to interpolate between
-    const [theta1, rho1] = playerPreviewData[index];
-    const [theta2, rho2] = playerPreviewData[index + 1];
-    
-    // Interpolate theta (handle angle wrapping)
-    let deltaTheta = theta2 - theta1;
-    if (deltaTheta > Math.PI) deltaTheta -= 2 * Math.PI;
-    if (deltaTheta < -Math.PI) deltaTheta += 2 * Math.PI;
-    
-    const interpolatedTheta = theta1 + deltaTheta * fraction;
-    const interpolatedRho = rho1 + (rho2 - rho1) * fraction;
-    
-    return [interpolatedTheta, interpolatedRho];
-}
-
-// Draw loading state on canvas
-function drawLoadingState(ctx) {
-    if (!ctx) return;
-
-    const canvas = ctx.canvas;
-    // Must match the pixelRatio used when setting canvas size
-    const pixelRatio = Math.min(window.devicePixelRatio || 1, 1.5);
-    const containerSize = canvas.width / pixelRatio;
-    const center = containerSize / 2;
-
-    ctx.save();
-
-    // Clear canvas
-    ctx.clearRect(0, 0, canvas.width, canvas.height);
-
-    // Create circular clipping path
-    ctx.beginPath();
-    ctx.arc(canvas.width/2, canvas.height/2, canvas.width/2, 0, Math.PI * 2);
-    ctx.clip();
-
-    // Setup coordinate system
-    ctx.scale(pixelRatio, pixelRatio);
-
-    // Draw loading text only
-    ctx.fillStyle = '#9ca3af';
-    ctx.font = '16px sans-serif';
-    ctx.textAlign = 'center';
-    ctx.textBaseline = 'middle';
-    ctx.fillText('Loading pattern...', center, center);
-
-    ctx.restore();
-}
-
-// Draw player preview for modal
-function drawPlayerPreview(ctx, progress) {
-    if (!ctx || !playerPreviewData || playerPreviewData.length === 0) return;
-    
-    const canvas = ctx.canvas;
-    // Must match the pixelRatio used when setting canvas size
-    const pixelRatio = Math.min(window.devicePixelRatio || 1, 1.5);
-    const containerSize = canvas.width / pixelRatio;
-    const center = containerSize / 2;
-    const scale = (containerSize / 2) - 30;
-    
-    ctx.save();
-
-    // Clear canvas for fresh drawing
-    ctx.clearRect(0, 0, canvas.width, canvas.height);
-    
-    // Create circular clipping path
-    ctx.beginPath();
-    ctx.arc(canvas.width/2, canvas.height/2, canvas.width/2, 0, Math.PI * 2);
-    ctx.clip();
-    
-    // Setup coordinate system for drawing
-    ctx.scale(pixelRatio, pixelRatio);
-    
-    // Calculate how many points to draw
-    const totalPoints = playerPreviewData.length;
-    const pointsToDraw = Math.floor(totalPoints * progress);
-    
-    if (pointsToDraw < 2) {
-        ctx.restore();
-        return;
-    }
-    
-    // Draw the pattern
-    ctx.beginPath();
-    ctx.strokeStyle = '#808080';
-    ctx.lineWidth = 1;
-    ctx.lineCap = 'round';
-    ctx.lineJoin = 'round';
-    
-    // Enable high quality rendering
-    ctx.imageSmoothingEnabled = true;
-    ctx.imageSmoothingQuality = 'high';
-    
-    // Draw pattern lines up to the last complete segment
-    for (let i = 0; i < pointsToDraw - 1; i++) {
-        const [theta1, rho1] = playerPreviewData[i];
-        const [theta2, rho2] = playerPreviewData[i + 1];
-        
-        const x1 = center + rho1 * scale * Math.cos(theta1);
-        const y1 = center + rho1 * scale * Math.sin(theta1);
-        const x2 = center + rho2 * scale * Math.cos(theta2);
-        const y2 = center + rho2 * scale * Math.sin(theta2);
-        
-        if (i === 0) {
-            ctx.moveTo(x1, y1);
-        }
-        ctx.lineTo(x2, y2);
-    }
-    
-    // Draw the final partial segment to the interpolated position
-    if (pointsToDraw > 0) {
-        const interpolatedCoord = getInterpolatedCoordinate(progress);
-        
-        if (interpolatedCoord && pointsToDraw > 1) {
-            // Get the last complete coordinate
-            const [lastTheta, lastRho] = playerPreviewData[pointsToDraw - 1];
-            const lastX = center + lastRho * scale * Math.cos(lastTheta);
-            const lastY = center + lastRho * scale * Math.sin(lastTheta);
-            
-            // Draw line to interpolated position
-            const [interpTheta, interpRho] = interpolatedCoord;
-            const interpX = center + interpRho * scale * Math.cos(interpTheta);
-            const interpY = center + interpRho * scale * Math.sin(interpTheta);
-            
-            ctx.lineTo(interpX, interpY);
-        }
-    }
-    
-    ctx.stroke();
-    
-    // Draw current position dot with interpolated position
-    if (pointsToDraw > 0) {
-        const interpolatedCoord = getInterpolatedCoordinate(progress);
-        
-        if (interpolatedCoord) {
-            const [theta, rho] = interpolatedCoord;
-            const x = center + rho * scale * Math.cos(theta);
-            const y = center + rho * scale * Math.sin(theta);
-            
-            // Draw white border
-            ctx.beginPath();
-            ctx.fillStyle = '#ffffff';
-            ctx.arc(x, y, 7.5, 0, Math.PI * 2);
-            ctx.fill();
-            
-            // Draw red dot
-            ctx.beginPath();
-            ctx.fillStyle = '#ff0000';
-            ctx.arc(x, y, 6, 0, Math.PI * 2);
-            ctx.fill();
-        }
-    }
-
-    ctx.restore();
-}
-
-// Load pattern coordinates for player preview
-async function loadPlayerPreviewData(pattern) {
-    try {
-        const response = await fetch('/get_theta_rho_coordinates', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ file_name: pattern })
-        });
-        
-        if (!response.ok) {
-            throw new Error(`HTTP error! status: ${response.status}`);
-        }
-        
-        const data = await response.json();
-        if (data.error) {
-            throw new Error(data.error);
-        }
-        
-        playerPreviewData = data.coordinates;
-        // Store the filename for comparison
-        playerPreviewData.fileName = normalizeFilePath(pattern);
-        
-    } catch (error) {
-        console.error(`Error loading player preview data: ${error.message}`);
-        playerPreviewData = null;
-    }
-}
-
-// Ultra-smooth animation function for modal
-function animateModalPreview() {
-    const modal = document.getElementById('playerPreviewModal');
-    if (!modal || modal.classList.contains('hidden')) return;
-    
-    const canvas = document.getElementById('playerPreviewCanvas');
-    const ctx = canvas.getContext('2d');
-    if (!ctx || !playerPreviewData) return;
-    
-    const currentTime = Date.now();
-    const elapsed = currentTime - modalAnimationStartTime;
-    const totalDuration = animationDuration;
-    
-    // Calculate smooth progress with easing
-    const rawProgress = Math.min(elapsed / totalDuration, 1);
-    const easeProgress = rawProgress < 0.5 
-        ? 2 * rawProgress * rawProgress 
-        : 1 - Math.pow(-2 * rawProgress + 2, 2) / 2;
-    
-    // Interpolate between last and target progress
-    const currentProgress = modalLastProgress + (modalTargetProgress - modalLastProgress) * easeProgress;
-    
-    // Draw the pattern up to current progress
-    drawPlayerPreview(ctx, currentProgress);
-    
-    // Continue animation if not at target
-    if (rawProgress < 1) {
-        modalAnimationId = requestAnimationFrame(animateModalPreview);
-    }
-}
-
-// Update modal preview with smooth animation
-function updateModalPreviewSmooth(newProgress) {
-    if (newProgress === modalTargetProgress) return; // No change needed
-    
-    // Stop any existing animation
-    if (modalAnimationId) {
-        cancelAnimationFrame(modalAnimationId);
-    }
-    
-    // Update animation parameters
-    modalLastProgress = modalTargetProgress;
-    modalTargetProgress = newProgress;
-    modalAnimationStartTime = Date.now();
-    
-    // Start smooth animation
-    animateModalPreview();
-}
-
-// Setup player preview modal events
-function setupPlayerPreviewModalEvents() {
-    const modal = document.getElementById('playerPreviewModal');
-    const closeBtn = document.getElementById('closePlayerPreview');
-    const toggleBtn = document.getElementById('toggle-preview-modal-btn');
-    
-    if (!modal || !closeBtn || !toggleBtn) return;
-    
-    // Remove any existing event listeners to prevent conflicts
-    const newToggleBtn = toggleBtn.cloneNode(true);
-    toggleBtn.parentNode.replaceChild(newToggleBtn, toggleBtn);
-    
-    // Toggle button click handler
-    newToggleBtn.addEventListener('click', () => {
-        const isHidden = modal.classList.contains('hidden');
-        if (isHidden) {
-            openPlayerPreviewModal();
-
-        } else {
-            modal.classList.add('hidden');
-        }
-    });
-    
-    // Close modal when clicking close button
-    closeBtn.addEventListener('click', () => {
-        setModalVisibility(false, true);
-    });
-    
-    // Close modal when clicking outside
-    modal.addEventListener('click', (e) => {
-        if (e.target === modal) {
-            setModalVisibility(false, true);
-        }
-    });
-    
-    // Close modal with Escape key
-    document.addEventListener('keydown', (e) => {
-        if (e.key === 'Escape' && !modal.classList.contains('hidden')) {
-            setModalVisibility(false, true);
-        }
-    });
-    
-    // Setup modal control buttons
-    setupModalControls();
-}
-
-// Handle pause/resume toggle
-async function togglePauseResume() {
-    const pauseButton = document.getElementById('modal-pause-button');
-    if (!pauseButton) return;
-
-    try {
-        const pauseIcon = pauseButton.querySelector('.material-icons');
-        const isCurrentlyPaused = pauseIcon.textContent === 'play_arrow';
-
-        // Show immediate feedback
-        showStatusMessage(isCurrentlyPaused ? 'Resuming...' : 'Pausing...', 'info');
-
-        const endpoint = isCurrentlyPaused ? '/resume_execution' : '/pause_execution';
-        const response = await fetch(endpoint, { method: 'POST' });
-
-        if (!response.ok) throw new Error(`Failed to ${isCurrentlyPaused ? 'resume' : 'pause'}`);
-
-        // Show success message
-        showStatusMessage(isCurrentlyPaused ? 'Pattern resumed' : 'Pattern paused', 'success');
-    } catch (error) {
-        console.error('Error toggling pause:', error);
-        showStatusMessage('Failed to pause/resume pattern', 'error');
-    }
-}
-
-// Setup modal controls
-function setupModalControls() {
-    const pauseButton = document.getElementById('modal-pause-button');
-    const skipButton = document.getElementById('modal-skip-button');
-    const stopButton = document.getElementById('modal-stop-button');
-    const speedDisplay = document.getElementById('modal-speed-display');
-    const speedInput = document.getElementById('modal-speed-input');
-    
-    if (!pauseButton || !skipButton || !stopButton || !speedDisplay || !speedInput) return;
-    
-    // Pause button click handler
-    pauseButton.addEventListener('click', togglePauseResume);
-    
-    // Skip button click handler
-    skipButton.addEventListener('click', async () => {
-        try {
-            // Show immediate feedback
-            showStatusMessage('Skipping to next pattern...', 'info');
-
-            const response = await fetch('/skip_pattern', { method: 'POST' });
-            if (!response.ok) throw new Error('Failed to skip pattern');
-
-            // Show success message
-            showStatusMessage('Skipped to next pattern', 'success');
-        } catch (error) {
-            console.error('Error skipping pattern:', error);
-            showStatusMessage('Failed to skip pattern', 'error');
-        }
-    });
-    
-    // Stop button click handler
-    stopButton.addEventListener('click', async () => {
-        try {
-            // Show immediate feedback
-            showStatusMessage('Stopping...', 'info');
-
-            const response = await fetch('/stop_execution', { method: 'POST' });
-            if (!response.ok) throw new Error('Failed to stop pattern');
-            else {
-                // Show success message
-                showStatusMessage('Pattern stopped', 'success');
-
-                // Hide modal when stopping
-                const modal = document.getElementById('playerPreviewModal');
-                if (modal) modal.classList.add('hidden');
-            }
-        } catch (error) {
-            console.error('Error stopping pattern:', error);
-            showStatusMessage('Failed to stop pattern', 'error');
-        }
-    });
-    
-    // Speed display click to edit
-    speedDisplay.addEventListener('click', () => {
-        isEditingSpeed = true;
-        speedDisplay.classList.add('hidden');
-        speedInput.value = speedDisplay.textContent;
-        speedInput.classList.remove('hidden');
-        speedInput.focus();
-        speedInput.select();
-    });
-    
-    // Speed input handlers
-    speedInput.addEventListener('keydown', (e) => {
-        if (e.key === 'Enter') {
-            e.preventDefault();
-            exitModalSpeedEditMode(true);
-        } else if (e.key === 'Escape') {
-            e.preventDefault();
-            exitModalSpeedEditMode(false);
-        }
-    });
-    
-    speedInput.addEventListener('blur', () => {
-        exitModalSpeedEditMode(true);
-    });
-}
-
-// Exit modal speed edit mode
-async function exitModalSpeedEditMode(save = false) {
-    const speedDisplay = document.getElementById('modal-speed-display');
-    const speedInput = document.getElementById('modal-speed-input');
-    
-    if (!speedDisplay || !speedInput) return;
-    
-    isEditingSpeed = false;
-    speedInput.classList.add('hidden');
-    speedDisplay.classList.remove('hidden');
-    
-    if (save) {
-        const speed = parseInt(speedInput.value);
-        if (!isNaN(speed) && speed >= 1 && speed <= 5000) {
-            try {
-                const response = await fetch('/set_speed', {
-                    method: 'POST',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({ speed: speed })
-                });
-                const data = await response.json();
-                if (data.success) {
-                    speedDisplay.textContent = speed;
-                    showStatusMessage(`Speed set to ${speed} mm/s`, 'success');
-                } else {
-                    throw new Error(data.detail || 'Failed to set speed');
-                }
-            } catch (error) {
-                console.error('Error setting speed:', error);
-                showStatusMessage('Failed to set speed', 'error');
-            }
-        } else {
-            showStatusMessage('Please enter a valid speed (1-5000)', 'error');
-        }
-    }
-}
-
-// Helper function to clean up pattern names
-function getCleanPatternName(filePath) {
-    if (!filePath) return '';
-    const fileName = normalizeFilePath(filePath);
-    return fileName.split('/').pop().replace('.thr', '');
-}
-
-// Sync modal controls with player status
-function syncModalControls(status) {
-    // Pattern name - clean up to show only filename
-    const modalPatternName = document.getElementById('modal-pattern-name');
-    if (modalPatternName && status.current_file) {
-        modalPatternName.textContent = getCleanPatternName(status.current_file);
-    }
-    
-    // Pattern preview image
-    const modalPatternPreviewImg = document.getElementById('modal-pattern-preview-img');
-    if (modalPatternPreviewImg && status.current_file) {
-        const encodedFilename = normalizeFilePath(status.current_file).replace(/[\\/]/g, '--');
-        const previewUrl = `/preview/${encodedFilename}`;
-        modalPatternPreviewImg.src = previewUrl;
-    }
-    
-    // ETA or Pause Countdown
-    const modalEta = document.getElementById('modal-eta');
-    if (modalEta) {
-        // Check if we're in a pause between patterns
-        if (status.pause_time_remaining && status.pause_time_remaining > 0) {
-            const minutes = Math.floor(status.pause_time_remaining / 60);
-            const seconds = Math.floor(status.pause_time_remaining % 60);
-            modalEta.textContent = `Next in: ${minutes}:${seconds.toString().padStart(2, '0')}`;
-        } else if (status.progress && status.progress.remaining_time !== null) {
-            const minutes = Math.floor(status.progress.remaining_time / 60);
-            const seconds = Math.floor(status.progress.remaining_time % 60);
-            modalEta.textContent = `ETA: ${minutes}:${seconds.toString().padStart(2, '0')}`;
-        } else {
-            modalEta.textContent = 'ETA: calculating...';
-        }
-    }
-    
-    // Progress bar
-    const modalProgressBar = document.getElementById('modal-progress-bar');
-    if (modalProgressBar) {
-        if (status.progress && status.progress.percentage !== null) {
-            modalProgressBar.style.width = `${status.progress.percentage}%`;
-        } else {
-            modalProgressBar.style.width = '0%';
-        }
-    }
-    
-    // Next pattern - clean up to show only filename
-    const modalNextPattern = document.getElementById('modal-next-pattern');
-    if (modalNextPattern) {
-        if (status.playlist && status.playlist.next_file) {
-            modalNextPattern.textContent = getCleanPatternName(status.playlist.next_file);
-        } else {
-            modalNextPattern.textContent = 'None';
-        }
-    }
-    
-    // Pause button
-    const modalPauseBtn = document.getElementById('modal-pause-button');
-    if (modalPauseBtn) {
-        const pauseIcon = modalPauseBtn.querySelector('.material-icons');
-        if (status.is_paused) {
-            pauseIcon.textContent = 'play_arrow';
-        } else {
-            pauseIcon.textContent = 'pause';
-        }
-    }
-    
-    // Skip button visibility
-    const modalSkipBtn = document.getElementById('modal-skip-button');
-    if (modalSkipBtn) {
-        if (status.playlist && status.playlist.next_file) {
-            modalSkipBtn.classList.remove('invisible');
-        } else {
-            modalSkipBtn.classList.add('invisible');
-        }
-    }
-    
-    // Speed display
-    const modalSpeedDisplay = document.getElementById('modal-speed-display');
-    if (modalSpeedDisplay && status.speed && !isEditingSpeed) {
-        modalSpeedDisplay.textContent = status.speed;
-    }
-}
-
-// Toggle modal visibility
-function togglePreviewModal() {
-    const modal = document.getElementById('playerPreviewModal');
-    const toggleBtn = document.getElementById('toggle-preview-modal-btn');
-    
-    if (!modal || !toggleBtn) return;
-    
-    const isHidden = modal.classList.contains('hidden');
-    if (isHidden) {
-        openPlayerPreviewModal();
-    } else {
-        setModalVisibility(false, true);
-        toggleBtn.classList.remove('active-tab');
-        toggleBtn.classList.add('inactive-tab');
-    }
-}
-
-// Button event handlers
-document.addEventListener('DOMContentLoaded', async () => {
-    try {
-        // Initialize WebSocket connection
-        initializeWebSocket();
-        
-        // Setup player preview modal events
-        setupPlayerPreviewModalEvents();
-        
-        console.log('Player initialized successfully');
-    } catch (error) {
-        console.error(`Error during initialization: ${error.message}`);
-    }
-});
-
-// Initialize WebSocket connection
-function initializeWebSocket() {
-    connectWebSocket();
-}
-
-// Clean up WebSocket when page unloads to prevent memory leaks
-window.addEventListener('beforeunload', () => {
-    if (ws) {
-        // Disable reconnection before closing
-        ws.onclose = null;
-        ws.close();
-        ws = null;
-    }
-});
-
-// Add resize handler for responsive canvas with debouncing
-let resizeTimeout;
-window.addEventListener('resize', () => {
-    const canvas = document.getElementById('playerPreviewCanvas');
-    const modal = document.getElementById('playerPreviewModal');
-    
-    if (canvas && modal && !modal.classList.contains('hidden')) {
-        // Clear previous timeout
-        clearTimeout(resizeTimeout);
-        
-        // Debounce resize calls to avoid excessive updates
-        resizeTimeout = setTimeout(() => {
-            const ctx = canvas.getContext('2d');
-            setupPlayerPreviewCanvas(ctx);
-            drawPlayerPreview(ctx, targetProgress);
-        }, 16); // ~60fps update rate
-    }
-});
-
-// Handle file changes and reload preview data
-function handleFileChange(newFile) {
-    if (newFile !== currentPreviewFile) {
-        currentPreviewFile = newFile;
-        if (newFile) {
-            loadPlayerPreviewData(`./patterns/${newFile}`);
-        } else {
-            playerPreviewData = null;
-        }
-    }
-}
-
-// Cache All Previews Prompt functionality
-let cacheAllInProgress = false;
-
-function shouldShowCacheAllPrompt() {
-    // Check if we've already shown the prompt
-    const promptShown = localStorage.getItem('cacheAllPromptShown');
-    console.log('shouldShowCacheAllPrompt - promptShown:', promptShown);
-    return !promptShown;
-}
-
-function showCacheAllPrompt(forceShow = false) {
-    console.log('showCacheAllPrompt called, forceShow:', forceShow);
-    if (!forceShow && !shouldShowCacheAllPrompt()) {
-        console.log('Cache all prompt already shown, skipping');
-        return;
-    }
-    
-    const modal = document.getElementById('cacheAllPromptModal');
-    if (modal) {
-        console.log('Showing cache all prompt modal');
-        modal.classList.remove('hidden');
-        // Store whether this was forced (manually triggered)
-        modal.dataset.manuallyTriggered = forceShow.toString();
-    } else {
-        console.log('Cache all prompt modal not found');
-    }
-}
-
-function hideCacheAllPrompt() {
-    const modal = document.getElementById('cacheAllPromptModal');
-    if (modal) {
-        modal.classList.add('hidden');
-    }
-}
-
-function markCacheAllPromptAsShown() {
-    localStorage.setItem('cacheAllPromptShown', 'true');
-}
-
-function initializeCacheAllPrompt() {
-    const modal = document.getElementById('cacheAllPromptModal');
-    const skipBtn = document.getElementById('skipCacheAllBtn');
-    const startBtn = document.getElementById('startCacheAllBtn');
-    const closeBtn = document.getElementById('closeCacheAllBtn');
-    
-    if (!modal || !skipBtn || !startBtn || !closeBtn) {
-        return;
-    }
-
-    // Skip button handler
-    skipBtn.addEventListener('click', () => {
-        const wasManuallyTriggered = modal.dataset.manuallyTriggered === 'true';
-        hideCacheAllPrompt();
-        
-        // Only mark as shown if it was automatically shown (not manually triggered)
-        if (!wasManuallyTriggered) {
-            markCacheAllPromptAsShown();
-        }
-    });
-
-    // Close button handler (after completion)
-    closeBtn.addEventListener('click', () => {
-        const wasManuallyTriggered = modal.dataset.manuallyTriggered === 'true';
-        hideCacheAllPrompt();
-        
-        // Always mark as shown after successful completion
-        if (!wasManuallyTriggered) {
-            markCacheAllPromptAsShown();
-        }
-    });
-
-    // Start caching button handler
-    startBtn.addEventListener('click', async () => {
-        if (cacheAllInProgress) {
-            return;
-        }
-
-        cacheAllInProgress = true;
-        
-        // Hide buttons and show progress
-        document.getElementById('cacheAllButtons').classList.add('hidden');
-        document.getElementById('cacheAllProgress').classList.remove('hidden');
-
-        try {
-            await startCacheAllProcess();
-            
-            // Show completion message
-            document.getElementById('cacheAllProgress').classList.add('hidden');
-            document.getElementById('cacheAllComplete').classList.remove('hidden');
-        } catch (error) {
-            console.error('Error caching all previews:', error);
-            
-            // Show error and reset
-            document.getElementById('cacheAllProgressText').textContent = 'Error occurred during caching';
-            setTimeout(() => {
-                hideCacheAllPrompt();
-                markCacheAllPromptAsShown();
-            }, 3000);
-        } finally {
-            cacheAllInProgress = false;
-        }
-    });
-}
-
-async function startCacheAllProcess() {
-    try {
-        // Get list of patterns using cached function
-        const patterns = await getCachedPatternFiles();
-        
-        if (!patterns || patterns.length === 0) {
-            throw new Error('No patterns found');
-        }
-
-        const progressBar = document.getElementById('cacheAllProgressBar');
-        const progressText = document.getElementById('cacheAllProgressText');
-        const progressPercentage = document.getElementById('cacheAllProgressPercentage');
-        
-        let completed = 0;
-        const batchSize = 5; // Process in small batches to avoid overwhelming the server
-
-        for (let i = 0; i < patterns.length; i += batchSize) {
-            const batch = patterns.slice(i, i + batchSize);
-            
-            // Update progress text
-            progressText.textContent = `Caching previews... (${Math.min(i + batchSize, patterns.length)}/${patterns.length})`;
-            
-            // Process batch
-            const batchPromises = batch.map(async (pattern) => {
-                try {
-                    const previewResponse = await fetch('/preview_thr', {
-                        method: 'POST',
-                        headers: {
-                            'Content-Type': 'application/json',
-                        },
-                        body: JSON.stringify({ file_name: pattern })
-                    });
-                    
-                    if (previewResponse.ok) {
-                        const data = await previewResponse.json();
-                        if (data.preview_url) {
-                            // Pre-load the image to cache it
-                            return new Promise((resolve) => {
-                                const img = new Image();
-                                img.onload = () => resolve();
-                                img.onerror = () => resolve(); // Continue even if image fails
-                                img.src = data.preview_url;
-                            });
-                        }
-                    }
-                    return Promise.resolve();
-                } catch (error) {
-                    console.warn(`Failed to cache preview for ${pattern}:`, error);
-                    return Promise.resolve(); // Continue with other patterns
-                }
-            });
-
-            await Promise.all(batchPromises);
-            completed += batch.length;
-
-            // Update progress bar
-            const progress = Math.round((completed / patterns.length) * 100);
-            progressBar.style.width = `${progress}%`;
-            progressPercentage.textContent = `${progress}%`;
-
-            // Small delay between batches to prevent overwhelming the server
-            if (i + batchSize < patterns.length) {
-                await new Promise(resolve => setTimeout(resolve, 100));
-            }
-        }
-
-        progressText.textContent = `Completed! Cached ${patterns.length} previews.`;
-        
-    } catch (error) {
-        throw error;
-    }
-}
-
-// Function to be called after initial cache generation completes
-function onInitialCacheComplete() {
-    console.log('onInitialCacheComplete called');
-    // Show the cache all prompt after a short delay
-    setTimeout(() => {
-        console.log('Triggering cache all prompt after delay');
-        showCacheAllPrompt();
-    }, 1000);
-}
-
-// Initialize on DOM load
-document.addEventListener('DOMContentLoaded', () => {
-    initializeCacheAllPrompt();
-});
-
-// Make functions available globally for debugging
-window.onInitialCacheComplete = onInitialCacheComplete;
-window.showCacheAllPrompt = showCacheAllPrompt;
-window.testCacheAllPrompt = function() {
-    console.log('Manual test trigger');
-    // Clear localStorage for testing
-    localStorage.removeItem('cacheAllPromptShown');
-    showCacheAllPrompt();
-}; 

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 5
static/js/coloris.min.js


+ 0 - 405
static/js/image2sand-init.js

@@ -1,405 +0,0 @@
-/**
- * Image2Sand (https://github.com/orionwc/Image2Sand) Initialization
- * 
- * This script handles the initialization of the Image2Sand converter.
- * 
- */
-
-// Log types for message styling
-const LOG_TYPE = {
-    INFO: 'info',
-    SUCCESS: 'success',
-    WARNING: 'warning',
-    ERROR: 'error'
-};
-
-/**
- * Display a message to the user
- * @param {string} message - The message to display
- * @param {string} type - The type of message (info, success, warning, error)
- */
-function logMessage(message, type = LOG_TYPE.INFO) {
-    console.log(`[${type.toUpperCase()}] ${message}`);
-    
-    // Create message element
-    const messageElement = document.createElement('div');
-    messageElement.className = `fixed top-4 right-4 p-4 rounded-lg shadow-lg z-50 transition-opacity duration-300 ${
-        type === LOG_TYPE.ERROR ? 'bg-red-100 text-red-700' :
-        type === LOG_TYPE.SUCCESS ? 'bg-green-100 text-green-700' :
-        type === LOG_TYPE.WARNING ? 'bg-yellow-100 text-yellow-700' :
-        'bg-blue-100 text-blue-700'
-    }`;
-    messageElement.textContent = message;
-    
-    // Add to document
-    document.body.appendChild(messageElement);
-    
-    // Remove after 5 seconds
-    setTimeout(() => {
-        messageElement.style.opacity = '0';
-        setTimeout(() => messageElement.remove(), 300);
-    }, 5000);
-}
-
-// Global variables for the image converter
-let originalImage = null;
-let fileName = '';
-let convertedCoordinates = null;
-let currentImageData = null;
-
-/**
- * Open the image converter dialog with the selected image
- * @param {File} file - The image file to convert
- */
-function openImageConverter(file) {
-    if (!file) {
-        logMessage('No file selected for conversion.', LOG_TYPE.ERROR);
-        return;
-    }
-
-    // Check if the file is an image
-    if (!file.type.startsWith('image/')) {
-        // If not an image, let the original uploadThetaRho handle it
-        return;
-    }
-
-    // Store the original file name globally for later use
-    window._originalUploadedFileName = file.name;
-    
-    // Remove extension for display/use
-    fileName = file.name.replace(/\.[^/.]+$/, '');
-    
-    // Create an image element to load the file
-    const img = new Image();
-    img.onload = function() {
-        // Draw the image on the canvas
-        originalImage = img;
-        drawAndPrepImage(img);
-        
-        // Show the converter dialog
-        const overlay = document.getElementById('image-converter');
-        overlay.classList.remove('hidden');
-        overlay.classList.add('visible');
-
-        // Initialize the UI elements
-        initializeUI();
-        
-        // Convert the image with default settings
-        convertImage();
-    };
-    
-    img.onerror = function() {
-        logMessage(`Failed to load image: ${file.name}`, LOG_TYPE.ERROR);
-    };
-    
-    // Load the image from the file
-    img.src = URL.createObjectURL(file);
-}
-
-/**
- * Initialize UI elements for the image converter
- */
-function initializeUI() {
-    // Set up event listeners for UI controls
-    const epsilonSlider = document.getElementById('epsilon-slider');
-    const epsilonValueDisplay = document.getElementById('epsilon-value-display');
-    
-    epsilonSlider.addEventListener('input', function() {
-        epsilonValueDisplay.textContent = this.value;
-    });
-    
-    // Set up event listeners for other controls
-    //document.getElementById('epsilon-slider').addEventListener('change', convertImage);
-    //document.getElementById('dot-number').addEventListener('change', convertImage);
-    //document.getElementById('contour-mode').addEventListener('change', convertImage);
-    //document.getElementById('is-loop').addEventListener('change', convertImage);
-    //document.getElementById('no-shortcuts').addEventListener('change', convertImage);
-}
-
-/**
- * Save the converted pattern as a .thr file
- */
-async function saveConvertedPattern() {
-    convertedCoordinates = document.getElementById('polar-coordinates-textarea').value;
-    if (!convertedCoordinates) {
-        logMessage('No converted coordinates to save.', LOG_TYPE.ERROR);
-        return;
-    }
-    
-    try {
-        // Always use the original uploaded file name if available
-        let originalName = window._originalUploadedFileName;
-        if (!originalName || typeof originalName !== 'string') {
-            originalName = fileName || 'pattern';
-        }
-        // Remove extension
-        let baseName = originalName.replace(/\.[^/.]+$/, '');
-        // Replace spaces with underscores
-        baseName = baseName.replace(/\s+/g, '_').trim();
-        // Fallback if baseName is empty
-        if (!baseName) baseName = 'pattern';
-        let thrFileName = baseName + '.thr';
-        
-        // Create a Blob with the coordinates
-        const blob = new Blob([convertedCoordinates], { type: 'text/plain' });
-        
-        // Create a FormData object
-        const formData = new FormData();
-        formData.append('file', new File([blob], thrFileName, { type: 'text/plain' }));
-        
-        // Show processing indicator
-        const processingIndicator = document.getElementById('processing-status');
-        const processingMessage = document.getElementById('processing-message');
-        if (processingMessage) {
-            processingMessage.textContent = `Saving pattern as ${thrFileName}...`;
-        }
-        processingIndicator.classList.add('visible');
-        
-        // Upload the file
-        const response = await fetch('/upload_theta_rho', {
-            method: 'POST',
-            body: formData
-        });
-        
-        const result = await response.json();
-        if (result.success) {
-            const fileInput = document.getElementById('upload_file');
-            const finalFileName = 'custom_patterns/' + thrFileName;
-            logMessage(`Image converted and saved as ${finalFileName}`, LOG_TYPE.SUCCESS);
-
-            // Invalidate pattern files cache to include new file
-            invalidatePatternFilesCache();
-
-            // Close the converter dialog
-            closeImageConverter();
-
-            // clear the file input
-            if (fileInput) {
-                fileInput.value = '';
-            }
-
-            // Refresh the file list
-            await loadThetaRhoFiles();
-            
-            // Select the newly created file
-            const fileList = document.getElementById('theta_rho_files');
-            if (fileList) {
-                const listItems = fileList.querySelectorAll('li');
-                for (const item of listItems) {
-                    if (item.textContent === finalFileName) {
-                        selectFile(finalFileName, item);
-                        break;
-                    }
-                }
-            }
-        } else {
-            logMessage(`Failed to save pattern: ${result.error || 'Unknown error'}`, LOG_TYPE.ERROR);
-        }
-    } catch (error) {
-        logMessage(`Error saving pattern: ${error.message}`, LOG_TYPE.ERROR);
-    } finally {
-        // Hide processing indicator
-        document.getElementById('processing-status').classList.remove('visible');
-    }
-}
-
-/**
- * Clear a canvas
- * @param {string} canvasId - The ID of the canvas element to clear
- */
-function clearCanvas(canvasId) {
-    const canvas = document.getElementById(canvasId);
-    const ctx = canvas.getContext('2d');
-    ctx.clearRect(0, 0, canvas.width, canvas.height);
-}
-
-/**
- * Close the image converter dialog
- */
-function closeImageConverter() {
-    const overlay = document.getElementById('image-converter');
-    overlay.classList.remove('visible');
-    overlay.classList.add('hidden');
-
-    // Clear the canvases
-    clearCanvas('original-image');
-    clearCanvas('edge-image');
-    clearCanvas('dot-image');
-    clearCanvas('connect-image');
-    
-    // Reset variables
-    originalImage = null;
-    fileName = '';
-    convertedCoordinates = null;
-    currentImageData = null;
-    
-    // Disable the save button
-    //document.getElementById('save-pattern-button').disabled = true;
-}
-
-async function generateOpenAIImage(apiKey, prompt) {
-    if (isGeneratingImage) {
-        logMessage("Image is still generating - please don't press the button.", LOG_TYPE.INFO);
-    } else {
-        isGeneratingImage = true;
-        clearCanvas('original-image');
-        clearCanvas('edge-image');
-        clearCanvas('dot-image');
-        clearCanvas('connect-image');
-        document.getElementById('gen-image-button').disabled = true;
-        // Show processing indicator
-        const processingIndicator = document.getElementById('processing-status');
-        const processingMessage = document.getElementById('processing-message');
-        if (processingMessage) {
-            processingMessage.textContent = `Generating image...`;
-        }
-        processingIndicator.classList.add('visible');
-        try {
-
-            const fullPrompt = `Draw an image of the following: ${prompt}. Make the line black and the background white. The drawing should be a single line, don't add any additional details to the image.`;
-
-            const response = await fetch('https://api.openai.com/v1/images/generations', {
-                method: 'POST',
-                headers: {
-                    'Authorization': `Bearer ${apiKey}`,
-                    'Content-Type': 'application/json'
-                },
-                body: JSON.stringify({
-                    model: 'dall-e-3',
-                    prompt: fullPrompt,
-                    size: '1024x1024',
-                    quality: 'standard',
-                    response_format: 'b64_json', // Specify base64 encoding
-                    n: 1
-                })
-            });
-
-            const data = await response.json();
-            //const imageUrl = data.data[0].url;
-            if ('error' in data) {
-                throw new Error(data.error.message);
-            }
-            const imageData = data.data[0].b64_json;
-
-            //console.log("Image Data: ", imageData);
-
-            const imgElement = new Image();
-            imgElement.onload = function() {
-                // Draw the image on the canvas
-                originalImage = imgElement;
-                drawAndPrepImage(imgElement);
-                
-                // Convert the image with default settings
-                convertImage();
-            };
-            imgElement.src = `data:image/png;base64,${imageData}`;
-
-            //console.log(`Image generated successfully`);
-            logMessage('Image generated successfully', LOG_TYPE.SUCCESS);
-        } catch (error) {
-            //console.error('Image generation error:', error);
-            logMessage('Image generation error: ' + error, LOG_TYPE.ERROR);
-        }
-        isGeneratingImage = false;
-        document.getElementById('gen-image-button').disabled = false;
-        document.getElementById('processing-status').classList.remove('visible');
-    }
-
-}
-
-function regeneratePattern() {
-    const generateButton = document.getElementById('generate-button');
-
-    // Disable button & show existing loader
-    generateButton.disabled = true;
-    generateButton.classList.add('loading');
-    // Wrap convertImage() in a Promise
-    new Promise((resolve, reject) => {
-        try {
-            convertImage();
-            setTimeout(resolve, 1000);
-        } catch (error) {
-            reject(error);
-        }
-    })
-        .then(() => {
-            logMessage("Pattern regenerated successfully.", LOG_TYPE.SUCCESS);
-        })
-        .catch(error => {
-            logMessage("Error regenerating pattern: " + error.message, LOG_TYPE.ERROR);
-        })
-        .finally(() => {
-            // Re-enable button & hide loader
-            generateButton.disabled = false;
-            generateButton.classList.remove('loading');
-        });
-}
-
-// Override the uploadThetaRho function to handle image files
-const originalUploadThetaRho = window.uploadThetaRho;
-
-window.uploadThetaRho = async function() {
-    const fileInput = document.getElementById('upload_file');
-    const file = fileInput.files[0];
-    
-    if (!file) {
-        logMessage('No file selected for upload.', LOG_TYPE.ERROR);
-        return;
-    }
-    
-    // Check if the file is an image
-    if (file.type.startsWith('image/')) {
-        // Handle image files with the converter
-        openImageConverter(file);
-        return;
-    }
-    
-    // For non-image files, use the original function
-    await originalUploadThetaRho();
-};
-
-// Remove existing event listener and add a new one
-document.getElementById('gen-image-button')?.addEventListener('click', function() {    
-    let apiKey = document.getElementById('api-key')?.value || '';
-    const googlyEyes = document.getElementById('googly-eyes');
-    const promptElement = document.getElementById('prompt');
-    
-    // Add null checks
-    const promptValue = promptElement?.value || '';
-    const googlyEyesChecked = googlyEyes?.checked || false;
-    
-    const prompt = promptValue + (googlyEyesChecked ? ' with disproportionately large googly eyes' : '');
-    
-    // Show the converter dialog
-    const overlay = document.getElementById('image-converter');
-    if (overlay) {
-        overlay.classList.remove('hidden');
-        // Initialize the UI elements
-        initializeUI();
-        generateOpenAIImage(apiKey, prompt);
-    }
-});
-
-async function loadThetaRhoFiles() {
-    const fileList = document.getElementById('theta_rho_files');
-    if (!fileList) return;
-
-    // Clear the list
-    fileList.innerHTML = '';
-
-    try {
-        // Fetch the file list from your backend
-        const files = await getCachedPatternFiles();
-
-        // Populate the list
-        files.forEach(filename => {
-            const li = document.createElement('li');
-            li.textContent = filename;
-            fileList.appendChild(li);
-        });
-    } catch (error) {
-        const li = document.createElement('li');
-        li.textContent = 'Error loading file list';
-        fileList.appendChild(li);
-    }
-}

+ 0 - 1400
static/js/image2sand.js

@@ -1,1400 +0,0 @@
-/*
- * Image2Sand - Convert images to sand table coordinates
- * 
- * This script processes images and converts them to polar coordinates
- * according to the specified settings, supporting multiple output formats including:
- *  Default: HackPack Sand Garden .ino in this repository
- *  theta-rho format: for use with sand tables like Sisyphus and Dune Weaver Mini.
- * 
- * Note:
- *  For Dune Weaver Mini compatibility, this script uses continuous theta values
- *  that can exceed 2π (360 degrees). This allows the arm to make multiple revolutions
- *  without creating unintended circles in the patterns.
- */
-
-class PriorityQueue {
-    constructor() {
-        this.heap = [];
-    }
-
-    enqueue(priority, key) {
-        this.heap.push({ key, priority });
-        this._bubbleUp(this.heap.length - 1);
-    }
-
-    dequeue() {
-        const min = this.heap[0];
-        const end = this.heap.pop();
-        
-        if (this.heap.length > 0) {
-            this.heap[0] = end;
-            this._sinkDown(0);
-        }
-        
-        return min;
-    }
-
-    isEmpty() {
-        return this.heap.length === 0;
-    }
-
-    _bubbleUp(index) {
-        const element = this.heap[index];
-        
-        while (index > 0) {
-            const parentIndex = Math.floor((index - 1) / 2);
-            const parent = this.heap[parentIndex];
-            
-            if (element.priority >= parent.priority) break;
-            
-            this.heap[parentIndex] = element;
-            this.heap[index] = parent;
-            index = parentIndex;
-        }
-    }
-
-    _sinkDown(index) {
-        const length = this.heap.length;
-        const element = this.heap[index];
-        
-        while (true) {
-            const leftChildIndex = 2 * index + 1;
-            const rightChildIndex = 2 * index + 2;
-            let smallestChildIndex = null;
-            
-            if (leftChildIndex < length) {
-                if (this.heap[leftChildIndex].priority < element.priority) {
-                    smallestChildIndex = leftChildIndex;
-                }
-            }
-            
-            if (rightChildIndex < length) {
-                if (
-                    (smallestChildIndex === null && this.heap[rightChildIndex].priority < element.priority) ||
-                    (smallestChildIndex !== null && this.heap[rightChildIndex].priority < this.heap[leftChildIndex].priority)
-                ) {
-                    smallestChildIndex = rightChildIndex;
-                }
-            }
-            
-            if (smallestChildIndex === null) break;
-            
-            this.heap[index] = this.heap[smallestChildIndex];
-            this.heap[smallestChildIndex] = element;
-            index = smallestChildIndex;
-        }
-    }
-}
-
-
-let currentContourIndex = 0;
-let isFirstClick = true;
-let originalImageElement = null;
-let isGeneratingImage = false;
-let isGeneratingCoords = false;
-
-function drawAndPrepImage(imgElement) {
-    const canvas = document.getElementById('original-image');
-    const ctx = canvas.getContext('2d');
-    canvas.width = imgElement.naturalWidth;
-    canvas.height = imgElement.naturalHeight;
-    ctx.drawImage(imgElement, 0, 0);
-
-    // Set originalImageElement to the current image
-    originalImageElement = imgElement;
-
-    // Enable Generate button
-    document.getElementById('generate-button').disabled = false;
-}
-
-
-async function generateImage(apiKey, prompt, autoprocess) {
-    if (isGeneratingImage) {
-        document.getElementById('generation-status').textContent = "Image is still generating - please don't press the button."; 
-    } else {
-        isGeneratingImage = true;
-        document.getElementById('gen-image-button').disabled = true;
-        document.getElementById('generation-status').style.display = 'block';
-        try {
-
-            const fullPrompt = `Draw an image of the following: ${prompt}. Make the line black and the background white. The drawing should be a single line, don't add any additional details to the image.`;
-
-            const response = await fetch('https://api.openai.com/v1/images/generations', {
-                method: 'POST',
-                headers: {
-                    'Authorization': `Bearer ${apiKey}`,
-                    'Content-Type': 'application/json'
-                },
-                body: JSON.stringify({
-                    model: 'dall-e-3',
-                    prompt: fullPrompt,
-                    size: '1024x1024',
-                    quality: 'standard',
-                    response_format: 'b64_json', // Specify base64 encoding
-                    n: 1
-                })
-            });
-
-            const data = await response.json();
-            //const imageUrl = data.data[0].url;
-            if ('error' in data) {
-                throw new Error(data.error.message);
-            }
-            const imageData = data.data[0].b64_json;
-
-            console.log("Image Data: ", imageData);
-
-            const imgElement = new Image();
-            imgElement.onload = function() {
-                drawAndPrepImage(imgElement);
-                if (autoprocess) {
-                    convertImage();
-                }
-            };
-            imgElement.src = `data:image/png;base64,${imageData}`;
-
-            console.log(`Image generated successfully`);
-        } catch (error) {
-            console.error('Image generation error:', error);
-        }
-        isGeneratingImage = false;
-        document.getElementById('generation-status').style.display = 'none';
-        document.getElementById('generation-status').textContent = "Image is generating - please wait...";
-        document.getElementById('gen-image-button').disabled = false;
-    }
-
-}
-
-
-function handleImageUpload(event) {
-    const file = event.target.files[0];
-    const reader = new FileReader();
-
-    reader.onload = e => {
-        if (!originalImageElement) {
-            originalImageElement = new Image();
-            originalImageElement.id = 'uploaded-image';
-            originalImageElement.onload = () => {
-                drawAndPrepImage(originalImageElement);
-            };
-            document.getElementById('original-image').appendChild(originalImageElement);
-        }
-        originalImageElement.src = e.target.result;
-    };
-
-    reader.readAsDataURL(file);
-}
-
-
-function processImage(imgElement) {
-    document.getElementById('processing-status').style.display = 'block';
-    document.getElementById('generate-button').disabled = true;
-
-    // Use setTimeout to allow the UI to update
-    setTimeout(() => {
-        const src = cv.imread(imgElement), dst = new cv.Mat();
-        cv.cvtColor(src, src, cv.COLOR_RGBA2GRAY, 0);
-        cv.Canny(src, dst, 50, 150, 3, false);
-
-        // Add morphological operations
-        const kernel = cv.getStructuringElement(cv.MORPH_RECT, new cv.Size(3, 3));
-        cv.dilate(dst, dst, kernel);
-        cv.erode(dst, dst, kernel);
-        // Invert the colors of the detected edges image
-        cv.bitwise_not(dst, dst);
-
-        // Ensure edge-image canvas has the same dimensions as the original image
-        const edgeCanvas = document.getElementById('edge-image');
-        edgeCanvas.width = imgElement.naturalWidth;
-        edgeCanvas.height = imgElement.naturalHeight;
-
-        // Show the edge detection results
-        cv.imshow('edge-image', dst);
-        
-        // Make the canvas visible
-        edgeCanvas.classList.remove('hidden');
-        
-        // Hide the placeholder
-        const placeholder = edgeCanvas.parentElement.querySelector('.upload-placeholder');
-        if (placeholder) {
-            placeholder.style.display = 'none';
-        }
-        
-        // Switch to the edge detection tab
-        switchTab('edge', 'image-converter');
-
-        cv.bitwise_not(dst, dst);
-        generateDots(dst);
-        src.delete(); dst.delete();
-
-        // Hide the processing status label
-        document.getElementById('processing-status').style.display = 'none';
-        document.getElementById('generate-button').disabled = false;
-    }, 0);
-}
-
-
-function plotNextContour() {
-    const canvas = document.getElementById('plotcontours');
-    const ctx = canvas.getContext('2d');
-
-    if (isFirstClick) {
-        // Clear the canvas on first click
-        ctx.clearRect(0, 0, canvas.width, canvas.height);
-        isFirstClick = false;
-    }
-
-    console.log('Cur Contour: ', currentContourIndex + '/' + orderedContoursSave.length + ":", JSON.stringify(orderedContoursSave[currentContourIndex]));
-
-    if (currentContourIndex < orderedContoursSave.length) {
-        const contour = orderedContoursSave[currentContourIndex];
-        const baseColor = getRandomColor();
-        const [r, g, b] = hexToRgb(baseColor);
-        const length = contour.length;
-
-        contour.forEach((point, i) => {
-            if (i === 0) {
-                ctx.beginPath();
-                ctx.moveTo(point.x, point.y);
-            } else {
-                ctx.lineTo(point.x, point.y);
-
-                // Calculate color fade
-                const ratio = i / length;
-                const fadedColor = `rgb(${Math.round(r * (1 - ratio))}, ${Math.round(g * (1 - ratio))}, ${Math.round(b * (1 - ratio))})`;
-                ctx.strokeStyle = fadedColor;
-                ctx.lineWidth = 2;
-                ctx.stroke();
-                ctx.beginPath();
-                ctx.moveTo(point.x, point.y);
-            }
-        });
-
-        // Mark the start and end points
-        ctx.fillStyle = baseColor;
-        ctx.font = '12px Arial';
-
-        // Start point
-        ctx.fillText(`S${currentContourIndex + 1}`, contour[0].x, contour[0].y);
-        ctx.beginPath();
-        ctx.arc(contour[0].x, contour[0].y, 3, 0, 2 * Math.PI);
-        ctx.fill();
-
-        // End point
-        ctx.fillText(`E${currentContourIndex + 1}`, contour[contour.length - 1].x, contour[contour.length - 1].y);
-        ctx.beginPath();
-        ctx.arc(contour[contour.length - 1].x, contour[contour.length - 1].y, 3, 0, 2 * Math.PI);
-        ctx.fill();
-
-        // Label the contour with its number
-        const midPoint = contour[Math.floor(contour.length / 2)];
-        ctx.fillText(`${currentContourIndex + 1}`, midPoint.x, midPoint.y);
-
-        // Increment the contour index
-        currentContourIndex++;
-    } else {
-        alert("All contours have been plotted. Starting over.");
-        currentContourIndex = 0; // Reset the index
-        isFirstClick = true; // Reset the first click flag
-    }
-}
-
-
-function findNearestPoint(lastPoint, contours, visitedPoints) {
-    let nearestPoint = null;
-    let nearestDistance = Infinity;
-
-    contours.forEach(contour => {
-        contour.forEach(point => {
-            if (!point || visitedPoints.has(JSON.stringify(point))) return;
-
-            const distance = Math.sqrt(
-                Math.pow(lastPoint.x - point.x, 2) + 
-                Math.pow(lastPoint.y - point.y, 2)
-            );
-
-            if (distance < nearestDistance) {
-                nearestDistance = distance;
-                nearestPoint = point;
-            }
-        });
-    });
-
-    return nearestPoint;
-}
-
-
-function findMaximalCenter(points) {
-    const minX = Math.min(...points.map(p => p.x));
-    const maxX = Math.max(...points.map(p => p.x));
-    const minY = Math.min(...points.map(p => p.y));
-    const maxY = Math.max(...points.map(p => p.y));
-
-    const centerX = (minX + maxX) / 2;
-    const centerY = (minY + maxY) / 2;
-
-    const width = maxX - minX;
-    const height = maxY - minY;
-
-    return { centerX, centerY, width, height };
-}
-
-
-function calculateCentroid(points) {
-    let sumX = 0, sumY = 0;
-    points.forEach(p => {
-        sumX += p.x;
-        sumY += p.y;
-    });
-    return { x: sumX / points.length, y: sumY / points.length };
-}
-
-
-function calculateDistances(contours) {
-    const distances = [];
-
-    for (let i = 0; i < contours.length; i++) {
-        distances[i] = [];
-        for (let j = 0; j < contours.length; j++) {
-            if (i !== j) {
-                const startToStart = Math.hypot(contours[i][0].x - contours[j][0].x, contours[i][0].y - contours[j][0].y);
-                const startToEnd = Math.hypot(contours[i][0].x - contours[j][contours[j].length - 1].x, contours[i][0].y - contours[j][contours[j].length - 1].y);
-                const endToStart = Math.hypot(contours[i][contours[i].length - 1].x - contours[j][0].x, contours[i][contours[i].length - 1].y - contours[j][0].y);
-                const endToEnd = Math.hypot(contours[i][contours[i].length - 1].x - contours[j][contours[j].length - 1].x, contours[i][contours[i].length - 1].y - contours[j][contours[j].length - 1].y);
-                distances[i][j] = Math.min(startToStart, startToEnd, endToStart, endToEnd);
-            }
-        }
-    }
-
-    return distances;
-}
-
-
-function tspNearestNeighbor(distances, contours) {
-    const path = [0];
-    const visited = new Set([0]);
-
-    while (path.length < contours.length) {
-        let last = path[path.length - 1];
-        let nearest = -1;
-        let nearestDistance = Infinity;
-
-        for (let i = 0; i < contours.length; i++) {
-            if (!visited.has(i) && distances[last][i] < nearestDistance) {
-                nearestDistance = distances[last][i];
-                nearest = i;
-            }
-        }
-
-        if (nearest !== -1) {
-            path.push(nearest);
-            visited.add(nearest);
-        }
-    }
-
-    return path;
-}
-
-
-function reorderContours(contours, path) {
-    const orderedContours = [];
-    console.log("Path:", path); // Debugging
-
-    for (let i = 0; i < path.length; i++) {
-        const contourIndex = path[i];
-        let contour = contours[contourIndex];
-
-        // Determine the direction to use the contour
-        if (i > 0) {
-            const prevContour = orderedContours[orderedContours.length - 1];
-            const prevPoint = prevContour[prevContour.length - 1];
-            
-            if (isFullyClosed(contour)) {
-                // Contour is fully closed, so can move the startPoint
-                contour = reorderPointsForLoop(contour, startNear = prevPoint)
-            } else if (prevPoint && contour[0]) {
-                // Contour not fully closed, decide whether to reverse contour
-                const startToStart = Math.hypot(prevPoint.x - contour[0].x, prevPoint.y - contour[0].y);
-                const startToEnd = Math.hypot(prevPoint.x - contour[contour.length - 1].x, prevPoint.y - contour[contour.length - 1].y);
-
-                if (startToEnd < startToStart) {
-                    contour.reverse();
-                }
-            } else {
-                console.error('Previous point or current contour start point is undefined.', { prevPoint, currentStart: contour[0] });
-                continue; // Skip if any point is undefined
-            }
-        }
-
-        orderedContours.push(contour);
-    }
-
-    return orderedContours;
-}
-
-
-function findClosestPoint(contours, point) {
-    let minDistance = Infinity;
-    let closestPoint = null;
-
-    contours.forEach(contour => {
-        contour.forEach(pt => {
-            const distance = Math.hypot(point.x - pt.x, point.y - pt.y);
-            if (distance < minDistance) {
-                minDistance = distance;
-                closestPoint = pt;
-            }
-        });
-    });
-    return closestPoint;
-}
-
-
-function createGraphWithConnectionTypes(contours) {
-    const graph = [];
-    const nodeMap = new Map(); // Map to quickly find nodes by coordinates
-    const MAX_JUMP_CONNECTIONS = 10; // Limit the number of jump connections per node
-
-    // Create nodes for each point in the contours
-    contours.forEach(contour => {
-        contour.forEach(pt => {
-            const key = `${pt.x},${pt.y}`;
-            if (!nodeMap.has(key)) {
-                const node = { x: pt.x, y: pt.y, neighbors: [] };
-                graph.push(node);
-                nodeMap.set(key, node);
-            }
-        });
-    });
-
-    // Connect points within the same contour (regular path connections)
-    contours.forEach(contour => {
-        for (let i = 0; i < contour.length; i++) {
-            const key = `${contour[i].x},${contour[i].y}`;
-            const node = nodeMap.get(key);
-            
-            if (i > 0) {
-                const prevKey = `${contour[i - 1].x},${contour[i - 1].y}`;
-                const prevNode = nodeMap.get(prevKey);
-                
-                if (!node.neighbors.some(neighbor => neighbor.node === prevNode)) {
-                    node.neighbors.push({ node: prevNode, isJump: false });
-                    prevNode.neighbors.push({ node: node, isJump: false });
-                }
-            }
-        }
-    });
-
-    // Create a spatial index for efficient nearest neighbor search
-    const spatialIndex = [];
-    graph.forEach(node => {
-        spatialIndex.push({
-            node: node,
-            x: node.x,
-            y: node.y
-        });
-    });
-
-    // Connect nodes from different contours with jump connections, but limit the number
-    graph.forEach(nodeA => {
-        // Sort other nodes by distance to current node
-        const distances = spatialIndex
-            .filter(item => item.node !== nodeA)
-            .map(item => ({
-                node: item.node,
-                distance: Math.hypot(nodeA.x - item.node.x, nodeA.y - item.node.y)
-            }))
-            .sort((a, b) => a.distance - b.distance);
-        
-        // Only connect to the closest MAX_JUMP_CONNECTIONS nodes
-        distances.slice(0, MAX_JUMP_CONNECTIONS).forEach(({ node: nodeB, distance }) => {
-            if (!nodeA.neighbors.some(neighbor => neighbor.node === nodeB)) {
-                nodeA.neighbors.push({ node: nodeB, isJump: true, jumpDistance: distance });
-            }
-            if (!nodeB.neighbors.some(neighbor => neighbor.node === nodeA)) {
-                nodeB.neighbors.push({ node: nodeA, isJump: true, jumpDistance: distance });
-            }
-        });
-    });
-
-    return graph;
-}
-
-
-function addStartEndToGraph(graph, start, end) {
-    const nodeMap = new Map();
-    const MAX_CONNECTIONS = 10; // Limit the number of connections from start/end points
-    
-    // Create a map for faster node lookups
-    graph.forEach((node, index) => {
-        nodeMap.set(`${node.x},${node.y}`, { node, index });
-    });
-    
-    // Check if start and end points already exist in the graph
-    const startKey = `${start.x},${start.y}`;
-    const endKey = `${end.x},${end.y}`;
-    
-    let startIdx = nodeMap.has(startKey) ? nodeMap.get(startKey).index : graph.length;
-    let endIdx = nodeMap.has(endKey) ? nodeMap.get(endKey).index : (startIdx === graph.length ? graph.length + 1 : graph.length);
-    
-    // Add start point if it doesn't exist
-    if (!nodeMap.has(startKey)) {
-        const startNode = { x: start.x, y: start.y, neighbors: [] };
-        graph.push(startNode);
-        nodeMap.set(startKey, { node: startNode, index: startIdx });
-    }
-    
-    // Add end point if it doesn't exist
-    if (!nodeMap.has(endKey)) {
-        const endNode = { x: end.x, y: end.y, neighbors: [] };
-        graph.push(endNode);
-        nodeMap.set(endKey, { node: endNode, index: endIdx });
-    }
-    
-    // Find the closest nodes to connect to start and end
-    const startNode = graph[startIdx];
-    const endNode = graph[endIdx];
-    
-    // Calculate distances from start to all other nodes
-    const startDistances = [];
-    graph.forEach((node, idx) => {
-        if (idx !== startIdx) {
-            const distance = Math.hypot(start.x - node.x, start.y - node.y);
-            startDistances.push({ node, idx, distance });
-        }
-    });
-    
-    // Sort by distance and connect only to the closest MAX_CONNECTIONS nodes
-    startDistances.sort((a, b) => a.distance - b.distance);
-    startDistances.slice(0, MAX_CONNECTIONS).forEach(({ node, idx, distance }) => {
-        startNode.neighbors.push({ node, isJump: true, jumpDistance: distance });
-        node.neighbors.push({ node: startNode, isJump: true, jumpDistance: distance });
-    });
-    
-    // Calculate distances from end to all other nodes
-    const endDistances = [];
-    graph.forEach((node, idx) => {
-        if (idx !== endIdx) {
-            const distance = Math.hypot(end.x - node.x, end.y - node.y);
-            endDistances.push({ node, idx, distance });
-        }
-    });
-    
-    // Sort by distance and connect only to the closest MAX_CONNECTIONS nodes
-    endDistances.sort((a, b) => a.distance - b.distance);
-    endDistances.slice(0, MAX_CONNECTIONS).forEach(({ node, idx, distance }) => {
-        endNode.neighbors.push({ node, isJump: true, jumpDistance: distance });
-        node.neighbors.push({ node: endNode, isJump: true, jumpDistance: distance });
-    });
-    
-    return { startIdx, endIdx };
-}
-
-
-function dijkstraWithMinimalJumps(graph, startIdx, endIdx) {
-    const distances = Array(graph.length).fill(Infinity);
-    const previous = Array(graph.length).fill(null);
-    const totalJumpDistances = Array(graph.length).fill(Infinity);
-    const priorityQueue = new PriorityQueue();
-    const nodeIndices = new Map(); // Map to quickly find node indices
-    
-    // Create a map of node coordinates to indices for faster lookups
-    graph.forEach((node, index) => {
-        nodeIndices.set(`${node.x},${node.y}`, index);
-    });
-
-    distances[startIdx] = 0;
-    totalJumpDistances[startIdx] = 0;
-    priorityQueue.enqueue(0, startIdx);
-
-    while (!priorityQueue.isEmpty()) {
-        const { key: minDistanceNode } = priorityQueue.dequeue();
-
-        if (minDistanceNode === endIdx) break;
-
-        const currentNode = graph[minDistanceNode];
-        currentNode.neighbors.forEach(neighbor => {
-            // Use the map for faster node index lookup
-            const neighborKey = `${neighbor.node.x},${neighbor.node.y}`;
-            const neighborIdx = nodeIndices.get(neighborKey);
-            
-            const jumpDistance = neighbor.isJump ? neighbor.jumpDistance : 0;
-            const alt = distances[minDistanceNode] + Math.hypot(currentNode.x - neighbor.node.x, currentNode.y - neighbor.node.y);
-            const totalJumpDist = totalJumpDistances[minDistanceNode] + jumpDistance;
-
-            if (totalJumpDist < totalJumpDistances[neighborIdx] || (totalJumpDist === totalJumpDistances[neighborIdx] && alt < distances[neighborIdx])) {
-                distances[neighborIdx] = alt;
-                previous[neighborIdx] = minDistanceNode;
-                totalJumpDistances[neighborIdx] = totalJumpDist;
-                priorityQueue.enqueue(totalJumpDist, neighborIdx);
-            }
-        });
-    }
-
-    const path = [];
-    let u = endIdx;
-
-    while (u !== null) {
-        path.unshift({ x: graph[u].x, y: graph[u].y });
-        u = previous[u];
-    }
-
-    return path;
-}
-
-
-function adjustEpsilon(epsilon, pointsOver) {
-    if (pointsOver > 100) {
-        return epsilon + 0.5;
-    } else if (pointsOver <= 20) {
-        return epsilon + 0.1;
-    } else {
-        // Scale adjustment for points over the target between 20 and 100
-        let scale = (pointsOver - 20) / (100 - 20); // Normalized to range 0-1
-        return epsilon + 0.1 + 0.5 * scale;  // Adjust between 0.1 and 0.5
-    }
-}
-
-// Checks if a contour is nearly closed
-function isNearlyClosed(contour, percentThreshold = 0.1) {
-    // Get the bounding box of the contour
-    const rect = cv.boundingRect(contour);
-    const size = Math.sqrt(rect.width * rect.width + rect.height * rect.height);
-
-    // Calculate the distance between the first and last points
-    const startPoint = { x: contour.intPtr(0)[0], y: contour.intPtr(0)[1] };
-    const endPoint = { x: contour.intPtr(contour.rows - 1)[0], y: contour.intPtr(contour.rows - 1)[1] };
-    const distance = Math.sqrt((startPoint.x - endPoint.x) ** 2 + (startPoint.y - endPoint.y) ** 2);
-
-    // Use a threshold based on the size of the object
-    const threshold = size * percentThreshold;
-    return (distance < threshold);
-}
-
-// Check if contour is fully closed
-function isContourClosed(contour) {
-    // Calculate the distance between the first and last points
-    const startPoint = { x: contour.intPtr(0)[0], y: contour.intPtr(0)[1] };
-    const endPoint = { x: contour.intPtr(contour.rows - 1)[0], y: contour.intPtr(contour.rows - 1)[1] };
-    
-    return (startPoint === endPoint);
-}
-
-// Checks if a PointList has the same first and last point
-function isFullyClosed(points) {
-    return ((points[0].x === points[points.length - 1].x) && (points[0].y === points[points.length - 1].y));
-}
-
-// Closes a contour by adding the first point at the end
-function closeContour(points) {
-    if (points.length > 1 && (points[0].x !== points[points.length - 1].x || points[0].y !== points[points.length - 1].y)) {
-        points.push({ x: points[0].x, y: points[0].y });
-    }
-    return points;
-}
-
-function areContoursSimilar(contour1, contour2, similarityThreshold) {
-    // Calculate the bounding boxes of the contours
-    const rect1 = cv.boundingRect(contour1);
-    const rect2 = cv.boundingRect(contour2);
-
-    // Calculate the intersection of the bounding boxes
-    const x1 = Math.max(rect1.x, rect2.x);
-    const y1 = Math.max(rect1.y, rect2.y);
-    const x2 = Math.min(rect1.x + rect1.width, rect2.x + rect2.width);
-    const y2 = Math.min(rect1.y + rect1.height, rect2.y + rect2.height);
-
-    // Check if there is an intersection
-    const intersectionWidth = Math.max(0, x2 - x1);
-    const intersectionHeight = Math.max(0, y2 - y1);
-    const intersectionArea = intersectionWidth * intersectionHeight;
-
-    // Calculate the union of the bounding boxes
-    const area1 = rect1.width * rect1.height;
-    const area2 = rect2.width * rect2.height;
-    const unionArea = area1 + area2 - intersectionArea;
-
-    // Calculate the similarity based on the intersection over union (IoU)
-    const similarity = intersectionArea / unionArea;
-
-    return similarity > similarityThreshold;
-}
-
-function deduplicateContours(contours, similarityThreshold = 0.5) {
-    const uniqueContours = [];
-    for (let i = 0; i < contours.size(); i++) {
-        const contour = contours.get(i);
-        let isDuplicate = false;
-        for (let j = 0; j < uniqueContours.length; j++) {
-            if (areContoursSimilar(contour, uniqueContours[j], similarityThreshold)) {
-                isDuplicate = true;
-                break;
-            }
-        }
-        if (!isDuplicate) {
-            uniqueContours.push(contour);
-        }
-    }
-    return uniqueContours;
-}
-
-// Function to interpolate points along a straight line
-function interpolatePoints(startPoint, endPoint, numPoints) {
-    if (numPoints <= 2) return [startPoint, endPoint];
-    
-    const points = [];
-    for (let i = 0; i < numPoints; i++) {
-        const t = i / (numPoints - 1);
-        const x = startPoint.x + t * (endPoint.x - startPoint.x);
-        const y = startPoint.y + t * (endPoint.y - startPoint.y);
-        points.push({ x, y });
-    }
-    return points;
-}
-
-// Function to calculate the distance between two points
-function distanceBetweenPoints(p1, p2) {
-    return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
-}
-
-// Function to add interpolated points to a contour based on segment length
-function addInterpolatedPoints(points, epsilon) {
-    if (points.length <= 1) return points;
-    
-    const result = [];
-    for (let i = 0; i < points.length - 1; i++) {
-        const startPoint = points[i];
-        const endPoint = points[i + 1];
-        
-        // Calculate distance between points
-        const distance = distanceBetweenPoints(startPoint, endPoint);
-        
-        // Determine how many points to add based on distance and epsilon
-        // For longer segments and smaller epsilon values, we want more points
-        // The smaller the epsilon, the more detailed the contour, so we add more points
-        const pointsToAdd = Math.max(2, Math.ceil(distance / (epsilon * 5)));
-        
-        // Add interpolated points for this segment
-        const interpolated = interpolatePoints(startPoint, endPoint, pointsToAdd);
-        
-        // Add all points except the last one (to avoid duplicates)
-        if (i < points.length - 2) {
-            result.push(...interpolated.slice(0, -1));
-        } else {
-            // For the last segment, include the end point
-            result.push(...interpolated);
-        }
-    }
-    
-    return result;
-}
-
-function getOrderedContours(edgeImage, initialEpsilon, retrievalMode, maxPoints) {
-    const contours = new cv.MatVector(), hierarchy = new cv.Mat();
-    cv.findContours(edgeImage, contours, hierarchy, retrievalMode, cv.CHAIN_APPROX_SIMPLE);
-    console.log("# Contours: ", contours.size());
-
-    // Deduplicate contours
-    const uniqueContours = deduplicateContours(contours);
-
-    const maxIterations = 100;  // Maximum iterations to avoid infinite loop
-
-    let contourPoints = [];
-    let totalPoints = 0;
-    let epsilon = initialEpsilon;
-    let iterations = 0;
-
-    do {
-        totalPoints = 0;
-        contourPoints = [];
-
-        for (let i = 0; i < uniqueContours.length; i++) {
-            const contour = uniqueContours[i]; // Use [] to access array elements
-            const simplified = new cv.Mat();
-            cv.approxPolyDP(contour, simplified, epsilon, true);
-        
-            let points = [];
-            for (let j = 0; j < simplified.rows; j++) {
-                const point = simplified.intPtr(j);
-                points.push({ x: point[0], y: point[1] });
-            }
-            simplified.delete();
-        
-            if (points.length > 0) {  // Check for empty contours
-                if (isNearlyClosed(contour)) {  // Only close the contour if it's nearly closed
-                    points = closeContour(points);
-                }
-                
-                // We no longer interpolate points here - we'll do it later only if needed
-                
-                if (isFullyClosed(points)) {
-                    // Move starting point to nearest the center
-                    points = reorderPointsForLoop(points);
-                }
-                contourPoints.push(points);
-                totalPoints += points.length;
-            }
-        }        
-
-        if (totalPoints > maxPoints) {
-            let pointsOver = totalPoints - maxPoints;
-            epsilon = adjustEpsilon(epsilon, pointsOver);
-            iterations++;
-        }
-    } while (totalPoints > maxPoints && iterations < maxIterations);
-
-    if (totalPoints > maxPoints && iterations >= maxIterations) {
-        let flattenedPoints = contourPoints.flat();
-        contourPoints = [flattenedPoints.slice(0, maxPoints)];  // Take the first N points
-    }
-
-    if (contourPoints.length === 0) {
-        console.error("No valid contours found.");
-        return [];
-    }
-
-    // Calculate distances and find the best path
-    const distances = calculateDistances(contourPoints);
-    const path = tspNearestNeighbor(distances, contourPoints);
-    const orderedContours = reorderContours(contourPoints, path);
-
-    return orderedContours;
-}
-
-
-function getRandomColor() {
-    const letters = '0123456789ABCDEF';
-    let color = '#';
-    for (let i = 0; i < 6; i++) {
-        color += letters[Math.floor(Math.random() * 16)];
-    }
-    return color;
-}
-
-
-function resetCanvas(canvasId) {
-    const canvas = document.getElementById(canvasId);
-    const ctx = canvas.getContext('2d');
-    
-    // Store current dimensions
-    const width = canvas.width;
-    const height = canvas.height;
-    
-    // Clear the canvas
-    ctx.clearRect(0, 0, width, height);
-    
-    // Reset transformation matrix
-    ctx.setTransform(1, 0, 0, 1, 0, 0);
-    
-    // Ensure dimensions are maintained
-    canvas.width = width;
-    canvas.height = height;
-}
-
-
-function traceContours(orderedContours, isLoop = false, minimizeJumps = true) {
-    let result = [];
-    let pathsUsed = [...orderedContours];
-   
-    for (let i = 0; i < orderedContours.length - (isLoop ? 0 : 1); i++) {
-        const currentContour = orderedContours[i];
-        
-        // If looping, add 1st contour again
-        const nextContour = orderedContours[(i + 1) % orderedContours.length];
-        const start = currentContour[currentContour.length - 1];  // End of the current contour
-        const end = nextContour[0];  // Start of the next contour
-
-        let path = [];
-        if (minimizeJumps){
-            // Find path between contours
-            path = findPathWithMinimalJumpDistances(pathsUsed, start, end);
-        }
-        
-        result.push(currentContour);
-        if (path.length > 0) {  // Add the path only if it has points
-            result.push(path);
-            pathsUsed.push(path);  // Add the used path to the list of paths
-            //console.log('Added Path: ', i, JSON.stringify(path));
-        } else {
-            //console.log('No Path Needed', i)
-        }
-    }
-
-    // If not looping, add the last contour as it doesn't need a connecting path and wasn't added above
-    if (!isLoop) { result.push(orderedContours[orderedContours.length - 1]); }
-
-    return result;
-}
-
-
-function removeConsecutiveDuplicates(points) {
-    return points.filter((point, index) => {
-        if (index === 0) return true; // Keep the first point
-        const prevPoint = points[index - 1];
-        return !(point.x === prevPoint.x && point.y === prevPoint.y);
-    });
-}
-
-
-function findPathWithMinimalJumpDistances(contours, start, end) {
-    const graph = createGraphWithConnectionTypes(contours);
-    const { startIdx, endIdx } = addStartEndToGraph(graph, start, end);
-    const path = dijkstraWithMinimalJumps(graph, startIdx, endIdx);
-    return path;
-}
-
-
-function generateDots(edgeImage) {
-    // Reset the canvas before drawing the new image
-    resetCanvas('dot-image');
-    resetCanvas('connect-image');
-    
-    // Ensure canvases have the same dimensions
-    const dotCanvas = document.getElementById('dot-image');
-    const connectCanvas = document.getElementById('connect-image');
-    const originalCanvas = document.getElementById('original-image');
-    const edgeCanvas = document.getElementById('edge-image');
-    
-    if (originalImageElement) {
-        // Set all canvases to the same dimensions
-        dotCanvas.width = originalImageElement.naturalWidth;
-        dotCanvas.height = originalImageElement.naturalHeight;
-        connectCanvas.width = originalImageElement.naturalWidth;
-        connectCanvas.height = originalImageElement.naturalHeight;
-    }
-
-    const epsilon = parseFloat(document.getElementById('epsilon-slider').value),
-        contourMode = document.getElementById('contour-mode').value,
-        isLoop = document.getElementById('is-loop').checked,
-        minimizeJumps = document.getElementById('no-shortcuts').checked,
-        outputFormat = parseInt(document.getElementById('output-type').value),
-        maxPoints = parseInt(document.getElementById('dot-number').value);
-        // useGaussianBlur = document.getElementById('gaussian-blur-toggle').checked,
-    const retrievalMode = (contourMode == 'External') ?  cv.RETR_EXTERNAL : cv.RETR_TREE;    
-    
-    orderedContours = getOrderedContours(edgeImage, epsilon, retrievalMode, maxPoints);
-
-    console.log('Ordered Contours: ', JSON.stringify(orderedContours));
-
-    const tracedContours = traceContours(orderedContours, isLoop, minimizeJumps);
-    console.log('Traced: ', JSON.stringify(tracedContours));
-
-    // Only apply additional interpolation if .thr format (format 2) is selected
-    let processedContours;
-    if (outputFormat === 2) {
-        // Apply interpolation for .thr format which needs more points for straight lines
-        processedContours = tracedContours.map(contour => 
-            addInterpolatedPoints(contour, epsilon)
-        );
-    } else {
-        processedContours = tracedContours;
-    }
-
-    plotContours(processedContours);
-    // Save for future plotting
-    orderedContoursSave = processedContours;
-
-    let orderedPoints = processedContours.flat();
-
-    // Should always be the case for isLoop
-    if (isFullyClosed(orderedPoints) || isLoop) {
-        orderedPoints = reorderPointsForLoop(orderedPoints);
-    }
-
-    orderedPoints = removeConsecutiveDuplicates(orderedPoints);
-
-    // For final output - if last point is same as first point, drop it.
-    if (isFullyClosed(orderedPoints)) {
-        orderedPoints = [...orderedPoints.slice(0,orderedPoints.length-1)];
-    }
-
-    const polarPoints = drawDots(orderedPoints);
-    WriteCoords(polarPoints, outputFormat);
-    drawConnections(polarPoints);
-    document.getElementById('total-points').innerText = `(${orderedPoints.length} Points)`;
-}
-
-
-function WriteCoords(polarPoints, outputFormat = 0){
-    let formattedPolarPoints = '';
-    switch (outputFormat) {
-        case 0: //Default
-            // For Image2Sand.ino code, we normalize the theta values
-            // We'll use modulo for this format
-            formattedPolarPoints = polarPoints.map(p => {
-            const normalizedTheta = ((p.theta % 3600) + 3600) % 3600; // Ensure positive value between 0-3600
-            return `{${p.r.toFixed(0)},${normalizedTheta.toFixed(0)}}`;
-        }).join(',');
-            break;
-
-        case 1: //Single Byte
-            // For single byte format, we need to normalize the theta values
-            // We'll use modulo for this format since it's just for Arduino code
-            formattedPolarPoints = polarPoints.map(p => {
-                const normalizedTheta = ((p.theta % 3600) + 3600) % 3600; // Ensure positive value between 0-3600
-                return `{${Math.round(255 * p.r / 1000)},${Math.round(255 * normalizedTheta / 3600)}}`;
-            }).join(',');
-            break;
-
-        case 2: //.thr
-            // For .thr format, we keep the continuous theta values
-            // Convert from tenths of degrees back to radians
-            // Apply a 90° clockwise rotation by subtracting π/2 (900 in tenths of degrees) from theta
-            formattedPolarPoints = polarPoints.map(p => {
-                // Subtract 900 (90 degrees) to rotate clockwise
-                const rotatedTheta = p.theta - 900;
-                return `${(-rotatedTheta * Math.PI / 1800).toFixed(5)} ${(p.r / 1000).toFixed(5)}`;
-            }).join("\n");
-            break;
-
-        case 3: // whitespace (might cause problems as it outputs a space)
-            // For whitespace format, we need to normalize the theta values
-            // We'll use modulo for this format
-            formattedPolarPoints = polarPoints.map(p => {
-                const normalizedTheta = ((p.theta % 3600) + 3600) % 3600; // Ensure positive value between 0-3600
-                return `${Math.round(255 * p.r / 1000).toString(2).padStart(8,'0').replaceAll('0',' ').replaceAll('1',"\t")}${Math.round(255 * normalizedTheta / 3600).toString(2).padStart(8,'0').replaceAll('0',' ').replaceAll('1',"\t")}`;
-            }).join("\n");
-            break;
-
-        default: 
-            break;
-    }
-   
-    document.getElementById('polar-coordinates-textarea').value = formattedPolarPoints;
-    document.getElementById('simple-coords').textContent = formattedPolarPoints;
-    document.getElementById('simple-coords-title').style = 'visibility: hidden';
-}
-
-
-function reorderPointsForLoop(points, startNear = calculateCentroid(points)) {
-    let minDist = Infinity;
-    let startIndex = 0;
-
-    // Find the point nearest to the centroid
-    points.forEach((point, index) => {
-        const dist = Math.hypot(point.x - startNear.x, point.y - startNear.y);
-        if (dist < minDist) {
-            minDist = dist;
-            startIndex = index;
-        }
-    });
-
-    // Reorder points to start from the point nearest to the centroid
-    return removeConsecutiveDuplicates([...points.slice(startIndex), ...points.slice(0, startIndex+1)]);
-    
-}
-
-
-function drawDots(points) {
-    const canvas = document.getElementById('dot-image'), ctx = canvas.getContext('2d');
-    
-    // Set canvas dimensions to match the original image
-    if (originalImageElement) {
-        canvas.width = originalImageElement.naturalWidth;
-        canvas.height = originalImageElement.naturalHeight;
-    }
-    
-    ctx.clearRect(0, 0, canvas.width, canvas.height);
-
-    const width = canvas.width, height = canvas.height;
-    const scaleX = width / originalImageElement.width;
-    const scaleY = height / originalImageElement.height;
-    const scale = Math.min(scaleX, scaleY);
-
-    points = points.map(p => ({ x: (p.x) * scale, y: (p.y) * scale }));
-
-    points.forEach(point => {
-        ctx.beginPath();
-        ctx.arc(point.x, point.y, 2, 0, 2 * Math.PI);
-        ctx.fill();
-    });
-
-    const formattedPoints = points.map(p => `{${p.x.toFixed(2)}, ${p.y.toFixed(2)}}`).join(',\n');
-
-    // Calculate polar coordinates
-    const center = findMaximalCenter(points);
-
-    points = points.map(p => ({ x: p.x - center.centerX, y: p.y - center.centerY }));
-    
-    // Calculate initial angles for all points
-    let polarPoints = points.map(p => {
-        const r = Math.sqrt(p.x * p.x + p.y * p.y);
-        // Get the basic angle in radians
-        let theta = Math.atan2(p.y, p.x);
-        
-        // Adjust theta to align 0 degrees to the right and 90 degrees up by flipping the y-axis
-        theta = -theta;
-        
-        return { 
-            r: r * (1000 / Math.max(...points.map(p => Math.sqrt(p.x * p.x + p.y * p.y)))), 
-            theta: theta, // Store in radians initially
-            x: p.x,
-            y: p.y
-        };
-    });
-    
-    // Process points to create continuous theta values
-    for (let i = 1; i < polarPoints.length; i++) {
-        const prev = polarPoints[i-1];
-        const curr = polarPoints[i];
-        
-        // Calculate the difference between current and previous theta
-        let diff = curr.theta - prev.theta;
-        
-        // If the difference is greater than π, it means we've wrapped around counterclockwise
-        // Adjust by subtracting 2π
-        if (diff > Math.PI) {
-            curr.theta -= 2 * Math.PI;
-        }
-        // If the difference is less than -π, it means we've wrapped around clockwise
-        // Adjust by adding 2π
-        else if (diff < -Math.PI) {
-            curr.theta += 2 * Math.PI;
-        }
-    }
-    
-    // Convert to degrees * 10 for the final format
-    polarPoints = polarPoints.map(p => ({
-        r: p.r,
-        theta: p.theta * (1800 / Math.PI) // Convert radians to tenths of degrees
-    }));
-
-    return polarPoints;
-}
-
-
-function plotContours(orderedContours) {
-    const canvas = document.getElementById('plotcontours');
-    const ctx = canvas.getContext('2d');
-
-    ctx.clearRect(0, 0, canvas.width, canvas.height);
-
-    orderedContours.forEach((contour, index) => {
-        const baseColor = getRandomColor();
-        const [r, g, b] = hexToRgb(baseColor);
-        const length = contour.length;
-
-        contour.forEach((point, i) => {
-            if (i === 0) {
-                ctx.beginPath();
-                ctx.moveTo(point.x, point.y);
-            } else {
-                ctx.lineTo(point.x, point.y);
-
-                // Calculate color fade
-                const ratio = i / length;
-                const fadedColor = `rgb(${Math.round(r * (1 - ratio))}, ${Math.round(g * (1 - ratio))}, ${Math.round(b * (1 - ratio))})`;
-                ctx.strokeStyle = fadedColor;
-                ctx.lineWidth = 2;
-                ctx.stroke();
-                ctx.beginPath();
-                ctx.moveTo(point.x, point.y);
-            }
-        });
-
-        // Mark the start and end points
-        ctx.fillStyle = baseColor;
-        ctx.font = '12px Arial';
-
-        // Start point
-        ctx.fillText(`S${index + 1}`, contour[0].x, contour[0].y);
-        ctx.beginPath();
-        ctx.arc(contour[0].x, contour[0].y, 3, 0, 2 * Math.PI);
-        ctx.fill();
-
-        // End point
-        ctx.fillText(`E${index + 1}`, contour[contour.length - 1].x, contour[contour.length - 1].y);
-        ctx.beginPath();
-        ctx.arc(contour[contour.length - 1].x, contour[contour.length - 1].y, 3, 0, 2 * Math.PI);
-        ctx.fill();
-
-        // Label the contour with its number
-        const midPoint = contour[Math.floor(contour.length / 2)];
-        ctx.fillText(`${index + 1}`, midPoint.x, midPoint.y);
-    });
-}
-
-
-function hexToRgb(hex) {
-    const bigint = parseInt(hex.slice(1), 16);
-    return [
-        (bigint >> 16) & 255,
-        (bigint >> 8) & 255,
-        bigint & 255
-    ];
-}
-
-
-function drawConnections(polarPoints) {
-    const canvas = document.getElementById('connect-image'), ctx = canvas.getContext('2d');
-    ctx.clearRect(0, 0, canvas.width, canvas.height);
-
-    const width = canvas.width, height = canvas.height;
-
-    // Reset transformation matrix
-    ctx.setTransform(1, 0, 0, 1, 0, 0);
-
-    // Translate the context to the center of the canvas
-    ctx.translate(width / 2, height / 2);
-
-    // Scale the points based on the size of the original image
-    const scaleX = width / 2000; // Since the circle radius is 1000
-    const scaleY = height / 2000;
-    const scale = Math.min(scaleX, scaleY);
-
-    // Draw the outline circle
-    ctx.beginPath();
-    ctx.arc(0, 0, 1000 * scale, 0, 2 * Math.PI);
-    ctx.strokeStyle = 'black';
-    ctx.stroke();
-
-    // Draw the connections based on polar coordinates
-    for (let i = 0; i < polarPoints.length - 1; i++) {
-        // Calculate the color for each segment
-        const t = i / (polarPoints.length - 1);
-        const color = `hsl(${t * 270}, 100%, 50%)`; // 270 degrees covers red to violet
-        ctx.strokeStyle = color;
-
-        const p1 = polarPoints[i];
-        const p2 = polarPoints[i + 1];
-
-        // Convert from tenths of degrees to radians for visualization
-        const theta1 = p1.theta * Math.PI / 1800;
-        const theta2 = p2.theta * Math.PI / 1800;
-
-        // Adjust y-coordinate calculation to invert the y-axis
-        const x1 = p1.r * Math.cos(theta1) * scale;
-        const y1 = -p1.r * Math.sin(theta1) * scale;
-        const x2 = p2.r * Math.cos(theta2) * scale;
-        const y2 = -p2.r * Math.sin(theta2) * scale;
-
-        ctx.beginPath();
-        ctx.moveTo(x1, y1);
-        ctx.lineTo(x2, y2);
-        ctx.stroke();
-    }
-}
-
-
-function convertImage() {
-    originalImageElement && processImage(originalImageElement);
-}
-
-
-// Function to get URL parameters
-function getUrlParams() {
-    const params = new URLSearchParams(window.location.search);
-    return {
-        apikey: params.get('apikey'),
-        prompt: params.get('prompt'),
-        run: params.get('run')
-    };
-}
-
-
-// Function to fill inputs from URL parameters
-function fillInputsFromParams(params) {
-    if (params.apikey) {
-        document.getElementById('api-key').value = params.apikey;
-    }
-    if (params.prompt) {
-        document.getElementById('prompt').value = params.prompt;
-    }
-}
-
-
-function setDefaultsForAutoGenerate() {
-    document.getElementById('epsilon-slider').value = 0.5;
-    document.getElementById('dot-number').value = 300;
-    document.getElementById('no-shortcuts').checked = true;
-    document.getElementById('is-loop').checked = true;
-    document.getElementById('contour-mode').value = 'Tree';
-    hiddenResponse();
-}
-
-function hiddenResponse() {
-    document.getElementById('master-container').style = 'display: none;';
-    document.getElementById('simple-container').style = 'visibility: visible';
-}
-
-
-document.addEventListener('DOMContentLoaded', function() {
-    const fileInput = document.getElementById('file-input');
-    const fileButton = document.getElementById('file-button');
-    const fileNameDisplay = document.getElementById('file-name');
-    const generateButton = document.getElementById('generate-button');
-    const epsilonSlider = document.getElementById('epsilon-slider');
-    const epsilonValueDisplay = document.getElementById('epsilon-value-display');
-    const dotNumberInput = document.getElementById('dot-number');
-    const contourModeSelect = document.getElementById('contour-mode');
-    //const gaussianBlurToggle = document.getElementById('gaussian-blur-toggle');
-
-    document.getElementById('plotButton').addEventListener('click', plotNextContour);
-
-    generateButton.addEventListener('click', convertImage);
-
-    fileButton.addEventListener('click', function() {
-        fileInput.click();
-    });
-
-    fileInput.addEventListener('change', function(event) {
-        const file = event.target.files[0];
-        if (file) {
-            fileNameDisplay.textContent = file.name;
-            if (typeof openImageConverter === 'function') {
-                openImageConverter(file);
-            } else {
-                handleImageUpload(event);
-            }
-        }
-    });
-
-    epsilonSlider.addEventListener('input', function() {
-        epsilonValueDisplay.textContent = epsilonSlider.value;
-    });
-
-    window.showTab = function(tabName) {
-        const tabContents = document.querySelectorAll('.tab-content');
-        tabContents.forEach(content => { content.style.display = 'none'; });
-        document.getElementById(tabName).style.display = 'block'; 
-    };
-
-});
-
-
-// Initialize the page with URL parameters if present
-document.addEventListener('DOMContentLoaded', (event) => {
-    const { apikey, prompt, run } = getUrlParams();
-
-    // Fill inputs with URL parameters if they exist
-    fillInputsFromParams({ apikey, prompt });
-    if (apikey) {  
-        document.getElementById('api-key-group').style.display = 'none'; 
-    }
-
-    // Generate image if all parameters are present
-    if (apikey && prompt && run) {
-        setDefaultsForAutoGenerate();
-        generateImage(apikey, prompt, run);
-        convertImage();
-    }
-
-    // Add event listener to the button inside the DOMContentLoaded event
-    const genImageButton = document.getElementById('gen-image-button');
-    if (genImageButton) {
-        genImageButton.addEventListener('click', () => {
-            const apiKeyElement = document.getElementById('api-key');
-            const promptElement = document.getElementById('prompt');
-            const googlyEyes = document.getElementById('googly-eyes');
-            
-            // Add null checks
-            const apiKey = apiKeyElement?.value || '';
-            const promptValue = promptElement?.value || '';
-            const googlyEyesChecked = googlyEyes?.checked || false;
-            
-            const prompt = promptValue + (googlyEyesChecked ? ' with disproportionately large googly eyes' : '');
-            generateImage(apiKey, prompt, false);
-        });
-    }
-    
-});

+ 0 - 2451
static/js/index.js

@@ -1,2451 +0,0 @@
-// Global variables
-let allPatterns = [];
-let allPatternsWithMetadata = []; // Enhanced pattern data with metadata
-let currentSort = { field: 'name', direction: 'asc' };
-let currentFilters = { category: 'all' };
-
-// AbortController for cancelling in-flight requests when navigating away
-let metadataAbortController = null;
-
-// Helper function to normalize file paths for cross-platform compatibility
-function normalizeFilePath(filePath) {
-    if (!filePath) return '';
-    // First normalize path separators
-    let normalized = filePath.replace(/\\/g, '/');
-    
-    // Remove only the patterns directory prefix, not patterns within the path
-    if (normalized.startsWith('./patterns/')) {
-        normalized = normalized.substring(11);
-    } else if (normalized.startsWith('patterns/')) {
-        normalized = normalized.substring(9);
-    }
-    
-    return normalized;
-}
-let selectedPattern = null;
-let previewObserver = null;
-let currentBatch = 0;
-const BATCH_SIZE = 40; // Increased batch size for better performance
-let previewCache = new Map(); // Simple in-memory cache for preview data
-let imageCache = new Map(); // Cache for preloaded images
-
-// Global variables for lazy loading
-let pendingPatterns = new Map(); // pattern -> element mapping
-let batchTimeout = null;
-const INITIAL_BATCH_SIZE = 12; // Smaller initial batch for faster first load
-const LAZY_BATCH_SIZE = 5; // Reduced batch size for smoother loading
-const MAX_RETRIES = 3; // Maximum number of retries for failed loads
-const RETRY_DELAY = 1000; // Delay between retries in ms
-
-// Shared caching for patterns list (persistent across sessions)
-const PATTERNS_CACHE_KEY = 'dune_weaver_patterns_cache';
-
-// IndexedDB cache for preview images with size management (shared with playlists page)
-const PREVIEW_CACHE_DB_NAME = 'dune_weaver_previews';
-const PREVIEW_CACHE_DB_VERSION = 1;
-const PREVIEW_CACHE_STORE_NAME = 'previews';
-const MAX_CACHE_SIZE_MB = 200;
-const MAX_CACHE_SIZE_BYTES = MAX_CACHE_SIZE_MB * 1024 * 1024;
-
-let previewCacheDB = null;
-
-// Define constants for log message types
-const LOG_TYPE = {
-    SUCCESS: 'success',
-    WARNING: 'warning',
-    ERROR: 'error',
-    INFO: 'info',
-    DEBUG: 'debug'
-};
-
-// Cache progress storage keys
-const CACHE_PROGRESS_KEY = 'dune_weaver_cache_progress';
-const CACHE_TIMESTAMP_KEY = 'dune_weaver_cache_timestamp';
-const CACHE_PROGRESS_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
-
-// Animated Preview Variables
-let animatedPreviewData = null;
-let animationFrameId = null;
-let isPlaying = false;
-let currentProgress = 0;
-let animationSpeed = 1;
-let lastTimestamp = 0;
-
-// Function to show status message
-function showStatusMessage(message, type = 'success') {
-    const statusContainer = document.getElementById('status-message-container');
-    const statusMessage = document.getElementById('status-message');
-    
-    if (!statusContainer || !statusMessage) return;
-    
-    // Set message and color based on type
-    statusMessage.textContent = message;
-    statusMessage.className = `text-base font-semibold opacity-0 transform -translate-y-2 transition-all duration-300 ease-in-out px-4 py-2 rounded-lg shadow-lg ${
-        type === 'success' ? 'bg-green-50 text-green-700 border border-green-200' :
-        type === 'error' ? 'bg-red-50 text-red-700 border border-red-200' :
-        type === 'warning' ? 'bg-yellow-50 text-yellow-700 border border-yellow-200' :
-        'bg-blue-50 text-blue-700 border border-blue-200'
-    }`;
-    
-    // Show message with animation
-    requestAnimationFrame(() => {
-        statusMessage.classList.remove('opacity-0', '-translate-y-2');
-        statusMessage.classList.add('opacity-100', 'translate-y-0');
-    });
-    
-    // Hide message after 5 seconds
-    setTimeout(() => {
-        statusMessage.classList.remove('opacity-100', 'translate-y-0');
-        statusMessage.classList.add('opacity-0', '-translate-y-2');
-    }, 5000);
-}
-
-// Function to log messages
-function logMessage(message, type = LOG_TYPE.DEBUG) {
-    console.log(`[${type}] ${message}`);
-}
-
-// Initialize IndexedDB for preview caching (shared with playlists page)
-async function initPreviewCacheDB() {
-    if (previewCacheDB) return previewCacheDB;
-    
-    return new Promise((resolve, reject) => {
-        const request = indexedDB.open(PREVIEW_CACHE_DB_NAME, PREVIEW_CACHE_DB_VERSION);
-        
-        request.onerror = () => {
-            logMessage('Failed to open preview cache database', LOG_TYPE.ERROR);
-            reject(request.error);
-        };
-        
-        request.onsuccess = () => {
-            previewCacheDB = request.result;
-            logMessage('Preview cache database opened successfully', LOG_TYPE.DEBUG);
-            resolve(previewCacheDB);
-        };
-        
-        request.onupgradeneeded = (event) => {
-            const db = event.target.result;
-            
-            // Create object store for preview cache
-            const store = db.createObjectStore(PREVIEW_CACHE_STORE_NAME, { keyPath: 'pattern' });
-            store.createIndex('lastAccessed', 'lastAccessed', { unique: false });
-            store.createIndex('size', 'size', { unique: false });
-            
-            logMessage('Preview cache database schema created', LOG_TYPE.DEBUG);
-        };
-    });
-}
-
-// Get preview from IndexedDB cache
-async function getPreviewFromCache(pattern) {
-    try {
-        if (!previewCacheDB) await initPreviewCacheDB();
-        
-        const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
-        const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-        
-        return new Promise((resolve, reject) => {
-            const request = store.get(pattern);
-            
-            request.onsuccess = () => {
-                const result = request.result;
-                if (result) {
-                    // Update last accessed time
-                    result.lastAccessed = Date.now();
-                    store.put(result);
-                    resolve(result.data);
-                } else {
-                    resolve(null);
-                }
-            };
-            
-            request.onerror = () => reject(request.error);
-        });
-    } catch (error) {
-        logMessage(`Error getting preview from cache: ${error.message}`, LOG_TYPE.WARNING);
-        return null;
-    }
-}
-
-// Clear a specific pattern from IndexedDB cache
-async function clearPatternFromIndexedDB(pattern) {
-    try {
-        if (!previewCacheDB) await initPreviewCacheDB();
-        
-        const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
-        const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-        
-        await new Promise((resolve, reject) => {
-            const deleteRequest = store.delete(pattern);
-            deleteRequest.onsuccess = () => {
-                logMessage(`Cleared ${pattern} from IndexedDB cache`, LOG_TYPE.DEBUG);
-                resolve();
-            };
-            deleteRequest.onerror = () => {
-                logMessage(`Failed to clear ${pattern} from IndexedDB: ${deleteRequest.error}`, LOG_TYPE.WARNING);
-                reject(deleteRequest.error);
-            };
-        });
-    } catch (error) {
-        logMessage(`Error clearing pattern from IndexedDB: ${error.message}`, LOG_TYPE.WARNING);
-    }
-}
-
-// Save preview to IndexedDB cache with size management
-async function savePreviewToCache(pattern, previewData) {
-    try {
-        if (!previewCacheDB) await initPreviewCacheDB();
-        
-        // Validate preview data before attempting to fetch
-        if (!previewData || !previewData.image_data) {
-            logMessage(`Invalid preview data for ${pattern}, skipping cache save`, LOG_TYPE.WARNING);
-            return;
-        }
-        
-        // Convert preview URL to blob for size calculation
-        const response = await fetch(previewData.image_data);
-        const blob = await response.blob();
-        const size = blob.size;
-        
-        // Check if we need to free up space
-        await managePreviewCacheSize(size);
-        
-        const cacheEntry = {
-            pattern: pattern,
-            data: previewData,
-            size: size,
-            lastAccessed: Date.now(),
-            created: Date.now()
-        };
-        
-        const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
-        const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-        
-        return new Promise((resolve, reject) => {
-            const request = store.put(cacheEntry);
-            
-            request.onsuccess = () => {
-                logMessage(`Preview cached for ${pattern} (${(size / 1024).toFixed(1)}KB)`, LOG_TYPE.DEBUG);
-                resolve();
-            };
-            
-            request.onerror = () => reject(request.error);
-        });
-        
-    } catch (error) {
-        logMessage(`Error saving preview to cache: ${error.message}`, LOG_TYPE.WARNING);
-    }
-}
-
-// Manage cache size by removing least recently used items
-async function managePreviewCacheSize(newItemSize) {
-    try {
-        const currentSize = await getPreviewCacheSize();
-        
-        if (currentSize + newItemSize <= MAX_CACHE_SIZE_BYTES) {
-            return; // No cleanup needed
-        }
-        
-        logMessage(`Cache size would exceed limit (${((currentSize + newItemSize) / 1024 / 1024).toFixed(1)}MB), cleaning up...`, LOG_TYPE.DEBUG);
-        
-        const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
-        const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-        const index = store.index('lastAccessed');
-        
-        // Get all entries sorted by last accessed (oldest first)
-        const entries = await new Promise((resolve, reject) => {
-            const request = index.getAll();
-            request.onsuccess = () => resolve(request.result);
-            request.onerror = () => reject(request.error);
-        });
-        
-        // Sort by last accessed time (oldest first)
-        entries.sort((a, b) => a.lastAccessed - b.lastAccessed);
-        
-        let freedSpace = 0;
-        const targetSpace = newItemSize + (MAX_CACHE_SIZE_BYTES * 0.1); // Free 10% extra buffer
-        
-        for (const entry of entries) {
-            if (freedSpace >= targetSpace) break;
-            
-            await new Promise((resolve, reject) => {
-                const deleteRequest = store.delete(entry.pattern);
-                deleteRequest.onsuccess = () => {
-                    freedSpace += entry.size;
-                    logMessage(`Evicted cached preview for ${entry.pattern} (${(entry.size / 1024).toFixed(1)}KB)`, LOG_TYPE.DEBUG);
-                    resolve();
-                };
-                deleteRequest.onerror = () => reject(deleteRequest.error);
-            });
-        }
-        
-        logMessage(`Freed ${(freedSpace / 1024 / 1024).toFixed(1)}MB from preview cache`, LOG_TYPE.DEBUG);
-        
-    } catch (error) {
-        logMessage(`Error managing cache size: ${error.message}`, LOG_TYPE.WARNING);
-    }
-}
-
-// Get current cache size
-async function getPreviewCacheSize() {
-    try {
-        if (!previewCacheDB) return 0;
-        
-        const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readonly');
-        const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-        
-        return new Promise((resolve, reject) => {
-            const request = store.getAll();
-            
-            request.onsuccess = () => {
-                const totalSize = request.result.reduce((sum, entry) => sum + (entry.size || 0), 0);
-                resolve(totalSize);
-            };
-            
-            request.onerror = () => reject(request.error);
-        });
-        
-    } catch (error) {
-        logMessage(`Error getting cache size: ${error.message}`, LOG_TYPE.WARNING);
-        return 0;
-    }
-}
-
-// Preload images in batch
-async function preloadImages(urls) {
-    const promises = urls.map(url => {
-        return new Promise((resolve, reject) => {
-            if (imageCache.has(url)) {
-                resolve(imageCache.get(url));
-                return;
-            }
-            const img = new Image();
-            img.onload = () => {
-                imageCache.set(url, img);
-                resolve(img);
-            };
-            img.onerror = reject;
-            img.src = url;
-        });
-    });
-    return Promise.allSettled(promises);
-}
-
-// Initialize Intersection Observer for lazy loading
-function initPreviewObserver() {
-    if (previewObserver) {
-        previewObserver.disconnect();
-    }
-
-    previewObserver = new IntersectionObserver((entries) => {
-        entries.forEach(entry => {
-            if (entry.isIntersecting) {
-                const previewContainer = entry.target;
-                const pattern = previewContainer.dataset.pattern;
-                if (pattern) {
-                    addPatternToBatch(pattern, previewContainer);
-                    previewObserver.unobserve(previewContainer);
-                }
-            }
-        });
-    }, {
-        rootMargin: '200px 0px',
-        threshold: 0.1
-    });
-}
-
-// Add pattern to pending batch for efficient loading
-async function addPatternToBatch(pattern, element) {
-    // Check in-memory cache first
-    if (previewCache.has(pattern)) {
-        const previewData = previewCache.get(pattern);
-        if (previewData && !previewData.error) {
-            if (element) {
-                updatePreviewElement(element, previewData.image_data);
-            }
-        }
-        return;
-    }
-
-    // Check IndexedDB cache
-    const cachedData = await getPreviewFromCache(pattern);
-    if (cachedData && !cachedData.error) {
-        // Add to in-memory cache for faster access
-        previewCache.set(pattern, cachedData);
-        if (element) {
-            updatePreviewElement(element, cachedData.image_data);
-        }
-        return;
-    }
-
-    // Check if this is a newly uploaded pattern
-    const isNewUpload = element?.dataset.isNewUpload === 'true';
-    
-    // Reset retry flags when starting fresh
-    if (element) {
-        element.dataset.retryCount = '0';
-        element.dataset.hasTriedIndividual = 'false';
-    }
-    
-    // Add loading indicator with better styling
-    if (!element.querySelector('img')) {
-        const loadingText = isNewUpload ? 'Generating preview...' : 'Loading...';
-        element.innerHTML = `
-            <div class="absolute inset-0 flex items-center justify-center bg-slate-100 rounded-full">
-                <div class="bg-slate-200 rounded-full h-8 w-8 flex items-center justify-center">
-                    <div class="bg-slate-500 rounded-full h-4 w-4"></div>
-                </div>
-            </div>
-            <div class="absolute inset-0 flex items-center justify-center">
-                <div class="text-xs text-slate-500 mt-12">${loadingText}</div>
-            </div>
-        `;
-    }
-
-    // Add to pending batch
-    pendingPatterns.set(pattern, element);
-    
-    // Process batch immediately if it's full or if it's a new upload
-    if (pendingPatterns.size >= LAZY_BATCH_SIZE || isNewUpload) {
-        processPendingBatch();
-    } else {
-        // Set a timeout to process smaller batches if they don't fill up
-        if (batchTimeout) {
-            clearTimeout(batchTimeout);
-        }
-        batchTimeout = setTimeout(() => {
-            if (pendingPatterns.size > 0) {
-                processPendingBatch();
-            }
-        }, 500); // Process after 500ms if batch doesn't fill up
-    }
-}
-
-// Update preview element with smooth transition
-function updatePreviewElement(element, imageUrl) {
-    const img = new Image();
-    img.onload = () => {
-        element.innerHTML = '';
-        element.appendChild(img);
-        img.className = 'w-full h-full object-contain transition-opacity duration-300';
-        img.style.opacity = '0';
-        requestAnimationFrame(() => {
-            img.style.opacity = '1';
-        });
-        // Mark element as loaded to prevent duplicate loading attempts
-        element.dataset.loaded = 'true';
-    };
-    img.src = imageUrl;
-    img.alt = 'Pattern Preview';
-}
-
-// Process pending patterns in batches
-async function processPendingBatch() {
-    if (pendingPatterns.size === 0) return;
-    
-    // Clear any pending timeout since we're processing now
-    if (batchTimeout) {
-        clearTimeout(batchTimeout);
-        batchTimeout = null;
-    }
-    
-    // Create a copy of current pending patterns and clear the original
-    const currentBatch = new Map(pendingPatterns);
-    pendingPatterns.clear();
-    
-    const patternsToLoad = Array.from(currentBatch.keys());
-    
-    try {
-        logMessage(`Loading batch of ${patternsToLoad.length} pattern previews`, LOG_TYPE.DEBUG);
-        
-        const response = await fetch('/preview_thr_batch', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ file_names: patternsToLoad })
-        });
-
-        if (response.ok) {
-            const results = await response.json();
-            
-            // Process all results
-            for (const [pattern, data] of Object.entries(results)) {
-                const element = currentBatch.get(pattern);
-                
-                if (data && !data.error && data.image_data) {
-                    // Cache in memory with size limit
-                    if (previewCache.size > 100) { // Limit cache size
-                        const oldestKey = previewCache.keys().next().value;
-                        previewCache.delete(oldestKey);
-                    }
-                    previewCache.set(pattern, data);
-                    
-                    // Save to IndexedDB cache for persistence
-                    await savePreviewToCache(pattern, data);
-                    
-                    if (element) {
-                        updatePreviewElement(element, data.image_data);
-                    }
-                } else {
-                    handleLoadError(pattern, element, data?.error || 'Failed to load preview');
-                }
-            }
-        }
-    } catch (error) {
-        logMessage(`Error loading preview batch: ${error.message}`, LOG_TYPE.ERROR);
-        
-        // Handle error for each pattern in batch
-        for (const pattern of patternsToLoad) {
-            const element = currentBatch.get(pattern);
-            handleLoadError(pattern, element, error.message);
-        }
-    }
-}
-
-// Trigger preview loading for currently visible patterns
-async function triggerPreviewLoadingForVisible() {
-    // Get all pattern cards currently in the DOM
-    const patternCards = document.querySelectorAll('.pattern-card');
-
-    // Collect all patterns that need checking
-    const patternsToCheck = [];
-    patternCards.forEach(card => {
-        const pattern = card.dataset.pattern;
-        const previewContainer = card.querySelector('.pattern-preview');
-
-        // Check if this pattern needs preview loading (only check in-memory cache here)
-        if (pattern && !previewCache.has(pattern) && !pendingPatterns.has(pattern)) {
-            patternsToCheck.push({ pattern, previewContainer });
-        }
-    });
-
-    // Wait for all IndexedDB cache checks to complete before processing batch
-    // This prevents unnecessary API calls for patterns that are already cached in IndexedDB
-    await Promise.all(patternsToCheck.map(({ pattern, previewContainer }) =>
-        addPatternToBatch(pattern, previewContainer)
-    ));
-
-    // Process any pending previews that weren't found in cache
-    if (pendingPatterns.size > 0) {
-        processPendingBatch();
-    }
-}
-
-// Load individual pattern preview (fallback when batch loading fails)
-async function loadIndividualPreview(pattern, element) {
-    try {
-        logMessage(`Loading individual preview for ${pattern}`, LOG_TYPE.DEBUG);
-        
-        const response = await fetch('/preview_thr_batch', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ file_names: [pattern] })
-        });
-
-        if (response.ok) {
-            const results = await response.json();
-            const data = results[pattern];
-            
-            if (data && !data.error && data.image_data) {
-                // Cache in memory with size limit
-                if (previewCache.size > 100) { // Limit cache size
-                    const oldestKey = previewCache.keys().next().value;
-                    previewCache.delete(oldestKey);
-                }
-                previewCache.set(pattern, data);
-                
-                // Save to IndexedDB cache for persistence
-                await savePreviewToCache(pattern, data);
-                
-                if (element) {
-                    updatePreviewElement(element, data.image_data);
-                }
-                
-                logMessage(`Individual preview loaded successfully for ${pattern}`, LOG_TYPE.DEBUG);
-            } else {
-                throw new Error(data?.error || 'Failed to load preview data');
-            }
-        } else {
-            throw new Error(`HTTP error! status: ${response.status}`);
-        }
-    } catch (error) {
-        logMessage(`Error loading individual preview for ${pattern}: ${error.message}`, LOG_TYPE.ERROR);
-        // Continue with normal error handling
-        handleLoadError(pattern, element, error.message);
-    }
-}
-
-// Handle load errors with retry logic
-function handleLoadError(pattern, element, error) {
-    const retryCount = element.dataset.retryCount || 0;
-    const isNewUpload = element.dataset.isNewUpload === 'true';
-    const hasTriedIndividual = element.dataset.hasTriedIndividual === 'true';
-    
-    // Use longer delays for newly uploaded patterns
-    const retryDelay = isNewUpload ? RETRY_DELAY * 2 : RETRY_DELAY;
-    const maxRetries = isNewUpload ? MAX_RETRIES * 2 : MAX_RETRIES;
-    
-    if (retryCount < maxRetries) {
-        // Update retry count
-        element.dataset.retryCount = parseInt(retryCount) + 1;
-        
-        // Determine retry strategy
-        let retryStrategy = 'batch';
-        if (retryCount >= 1 && !hasTriedIndividual) {
-            // After first batch attempt fails, try individual loading
-            retryStrategy = 'individual';
-            element.dataset.hasTriedIndividual = 'true';
-        }
-        
-        // Show retry message with different text for new uploads and retry strategies
-        let retryText;
-        if (isNewUpload) {
-            retryText = retryStrategy === 'individual' ? 
-                `Trying individual load... (${retryCount + 1}/${maxRetries})` :
-                `Generating preview... (${retryCount + 1}/${maxRetries})`;
-        } else {
-            retryText = retryStrategy === 'individual' ? 
-                `Trying individual load... (${retryCount + 1}/${maxRetries})` :
-                `Retrying... (${retryCount + 1}/${maxRetries})`;
-        }
-            
-        element.innerHTML = `
-            <div class="absolute inset-0 flex items-center justify-center bg-slate-100 rounded-full">
-                <div class="text-xs text-slate-500 text-center">
-                    <div>${isNewUpload ? 'Processing new pattern' : 'Failed to load'}</div>
-                    <div>${retryText}</div>
-                </div>
-            </div>
-        `;
-        
-        // Retry after delay with appropriate strategy
-        setTimeout(() => {
-            if (retryStrategy === 'individual') {
-                loadIndividualPreview(pattern, element);
-            } else {
-                addPatternToBatch(pattern, element);
-            }
-        }, retryDelay);
-    } else {
-        // Show final error state
-        element.innerHTML = `
-            <div class="absolute inset-0 flex items-center justify-center bg-slate-100 rounded-full">
-                <div class="text-xs text-slate-500 text-center">
-                    <div>Failed to load</div>
-                    <div>Click to retry</div>
-                </div>
-            </div>
-        `;
-        
-        // Add click handler for manual retry
-        element.onclick = () => {
-            element.dataset.retryCount = '0';
-            element.dataset.hasTriedIndividual = 'false';
-            addPatternToBatch(pattern, element);
-        };
-    }
-    
-    previewCache.set(pattern, { error: true });
-}
-
-// Load and display patterns
-async function loadPatterns(forceRefresh = false) {
-    try {
-        logMessage('Loading patterns...', LOG_TYPE.INFO);
-        
-        // First load basic patterns list for fast initial display
-        logMessage('Fetching basic patterns list from server', LOG_TYPE.DEBUG);
-        const basicPatterns = await getCachedPatternFiles(forceRefresh);
-        const thrPatterns = basicPatterns.filter(file => file.endsWith('.thr'));
-        logMessage(`Received ${thrPatterns.length} basic patterns from server`, LOG_TYPE.INFO);
-        
-        // Store basic patterns and display immediately
-        let patterns = [...thrPatterns];
-        allPatterns = patterns;
-        
-        // Sort patterns alphabetically to match final enhanced sorting
-        const sortedPatterns = patterns.sort((a, b) => a.localeCompare(b));
-
-        allPatterns = sortedPatterns;
-        
-        // Display basic patterns immediately for fast initial load
-        logMessage('Displaying initial patterns...', LOG_TYPE.INFO);
-        displayPatternBatch();
-        logMessage('Initial patterns loaded successfully.', LOG_TYPE.SUCCESS);
-        
-        // Load metadata in background for enhanced features
-        setTimeout(async () => {
-            try {
-                // Cancel any previous metadata loading request
-                if (metadataAbortController) {
-                    metadataAbortController.abort();
-                }
-                
-                // Create new AbortController for this request with timeout
-                metadataAbortController = new AbortController();
-
-                // Set a timeout to prevent hanging on slow Pi systems
-                const timeoutId = setTimeout(() => {
-                    metadataAbortController.abort();
-                    logMessage('Metadata loading timed out after 30 seconds', LOG_TYPE.WARNING);
-                }, 30000); // 30 second timeout
-
-                logMessage('Loading enhanced metadata...', LOG_TYPE.DEBUG);
-                const metadataResponse = await fetch('/list_theta_rho_files_with_metadata', {
-                    signal: metadataAbortController.signal,
-                    headers: {
-                        'Cache-Control': 'no-cache'
-                    }
-                });
-
-                // Clear timeout if request succeeds
-                clearTimeout(timeoutId);
-                const patternsWithMetadata = await metadataResponse.json();
-                
-                // Store enhanced patterns data
-                allPatternsWithMetadata = [...patternsWithMetadata];
-                
-                // Update category filter dropdown now that we have metadata
-                updateBrowseCategoryFilter();
-                
-                // Enable sort controls and display patterns consistently
-                enableSortControls();
-                
-                logMessage(`Enhanced metadata loaded for ${patternsWithMetadata.length} patterns`, LOG_TYPE.SUCCESS);
-                
-                // Clear the controller reference since request completed
-                metadataAbortController = null;
-            } catch (metadataError) {
-                if (metadataError.name === 'AbortError') {
-                    logMessage('Metadata loading cancelled or timed out', LOG_TYPE.WARNING);
-                } else {
-                    logMessage(`Failed to load enhanced metadata: ${metadataError.message}`, LOG_TYPE.WARNING);
-                }
-
-                // Create basic metadata from file list to populate categories
-                if (allPatterns && allPatterns.length > 0) {
-                    allPatternsWithMetadata = allPatterns.map(pattern => {
-                        const pathParts = pattern.split('/');
-                        const category = pathParts.length > 1 ? pathParts.slice(0, -1).join('/') : 'root';
-                        const fileName = pathParts[pathParts.length - 1].replace('.thr', '');
-                        return {
-                            path: pattern,
-                            name: fileName,
-                            category: category,
-                            date_modified: 0,
-                            coordinates_count: 0
-                        };
-                    });
-
-                    // Update category filter with basic data
-                    updateBrowseCategoryFilter();
-                    logMessage('Using basic category data (metadata unavailable)', LOG_TYPE.INFO);
-                }
-
-                metadataAbortController = null;
-            }
-        }, 100); // Small delay to let initial render complete
-        if (forceRefresh) {
-            showStatusMessage('Patterns list refreshed successfully', 'success');
-        }
-    } catch (error) {
-        logMessage(`Error loading patterns: ${error.message}`, LOG_TYPE.ERROR);
-        console.error('Full error:', error);
-        showStatusMessage('Failed to load patterns', 'error');
-    }
-}
-
-// Display a batch of patterns with improved initial load
-function displayPatternBatch() {
-    const patternGrid = document.querySelector('.grid');
-    if (!patternGrid) {
-        logMessage('Pattern grid not found in the DOM', LOG_TYPE.ERROR);
-        return;
-    }
-
-    const start = currentBatch * BATCH_SIZE;
-    const end = Math.min(start + BATCH_SIZE, allPatterns.length);
-    const batchPatterns = allPatterns.slice(start, end);
-
-    // Display batch patterns
-    batchPatterns.forEach(pattern => {
-        const patternCard = createPatternCard(pattern);
-        patternGrid.appendChild(patternCard);
-    });
-
-    // If there are more patterns to load, set up the observer for the last few cards
-    if (end < allPatterns.length) {
-        const lastCards = Array.from(patternGrid.children).slice(-3); // Observe last 3 cards
-        lastCards.forEach(card => {
-            const observer = new IntersectionObserver((entries) => {
-                if (entries[0].isIntersecting) {
-                    currentBatch++;
-                    displayPatternBatch();
-                    observer.disconnect();
-                }
-            }, {
-                rootMargin: '200px 0px',
-                threshold: 0.1
-            });
-            observer.observe(card);
-        });
-    }
-}
-
-// Create a pattern card element
-function createPatternCard(pattern) {
-    const card = document.createElement('div');
-    card.className = 'pattern-card group relative flex flex-col items-center gap-3 bg-gray-50';
-    card.style = 'max-width:128px; width:100%; min-width:80px;';
-    card.dataset.pattern = pattern;
-    
-    // Create preview container with proper styling for loading indicator
-    const previewContainer = document.createElement('div');
-    previewContainer.className = 'rounded-full shadow-md relative pattern-preview group aspect-square w-full max-w-[128px] min-w-[80px] overflow-hidden';
-    previewContainer.dataset.pattern = pattern;
-    
-    // Add loading indicator
-    previewContainer.innerHTML = '<div class="absolute inset-0 flex items-center justify-center"><div class="bg-slate-200 rounded-full h-8 w-8 flex items-center justify-center"><div class="bg-slate-500 rounded-full h-4 w-4"></div></div></div>';
-    
-    // Add play button overlay (centered, hidden by default, shown on hover)
-    const playOverlay = document.createElement('div');
-    playOverlay.className = 'absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200 cursor-pointer';
-    playOverlay.innerHTML = '<div class="bg-white rounded-full p-2 shadow-lg flex items-center justify-center w-10 h-10"><span class="material-icons text-lg text-gray-800">play_arrow</span></div>';
-    playOverlay.title = 'Preview pattern';
-    playOverlay.addEventListener('click', (e) => {
-        e.stopPropagation(); // Prevent card selection
-        openAnimatedPreview(pattern);
-    });
-    previewContainer.appendChild(playOverlay);
-    
-    // Add heart favorite button (top-right corner)
-    const heartButton = document.createElement('div');
-    const isAlreadyFavorite = favoritePatterns.has(pattern);
-    const heartOpacity = isAlreadyFavorite ? 'opacity-100' : 'opacity-0 group-hover:opacity-100';
-    heartButton.className = `absolute top-2 right-2 w-7 h-7 cursor-pointer ${heartOpacity} transition-opacity duration-200 z-10 bg-white/90 rounded-full shadow-sm flex items-center justify-center`;
-    const heartIcon = isAlreadyFavorite ? 'favorite' : 'favorite_border';
-    const heartColor = isAlreadyFavorite ? 'text-red-500 hover:text-red-600' : 'text-gray-400 hover:text-red-500';
-    heartButton.innerHTML = `<span class="material-icons text-lg ${heartColor} transition-colors" id="heart-${pattern.replace(/[^a-zA-Z0-9]/g, '_')}">${heartIcon}</span>`;
-    heartButton.title = isAlreadyFavorite ? 'Remove from favorites' : 'Add to favorites';
-    heartButton.addEventListener('click', (e) => {
-        e.stopPropagation(); // Prevent card selection
-        toggleFavorite(pattern);
-    });
-    // Note: Heart button will be added to card, not previewContainer to avoid circular clipping
-    
-    // Create pattern name
-    const patternName = document.createElement('p');
-    patternName.className = 'text-gray-700 text-sm font-medium text-center truncate w-full';
-    patternName.textContent = pattern.replace('.thr', '').split('/').pop();
-
-    // Add click handler
-    card.onclick = () => selectPattern(pattern, card);
-
-    // Check if preview is already in cache
-    const previewData = previewCache.get(pattern);
-    if (previewData && !previewData.error && previewData.image_data) {
-        updatePreviewElement(previewContainer, previewData.image_data);
-    } else {
-        // Start observing the preview container for lazy loading
-        previewObserver.observe(previewContainer);
-    }
-
-    card.appendChild(previewContainer);
-    card.appendChild(patternName);
-    
-    // Add heart button to card (not previewContainer) to avoid circular clipping
-    card.appendChild(heartButton);
-    
-    return card;
-}
-
-// Select a pattern
-function selectPattern(pattern, card) {
-    // Remove selected class from all cards
-    document.querySelectorAll('.pattern-card').forEach(c => {
-        c.classList.remove('selected');
-    });
-    
-    // Add selected class to clicked card
-    card.classList.add('selected');
-    
-    // Show pattern preview
-    showPatternPreview(pattern);
-}
-
-// Show pattern preview
-async function showPatternPreview(pattern) {
-    try {
-        // Check in-memory cache first
-        let data = previewCache.get(pattern);
-        
-        // If not in cache, fetch it
-        if (!data) {
-            const response = await fetch('/preview_thr_batch', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ file_names: [pattern] })
-            });
-
-            if (!response.ok) {
-                throw new Error(`HTTP error! status: ${response.status}`);
-            }
-
-            const results = await response.json();
-            data = results[pattern];
-            if (data && !data.error) {
-                // Cache in memory
-                previewCache.set(pattern, data);
-            } else {
-                throw new Error(data?.error || 'Failed to get preview data');
-            }
-        }
-
-        const previewPanel = document.getElementById('patternPreviewPanel');
-        const layoutContainer = document.querySelector('.layout-content-container');
-        
-        // Update preview content
-        if (data.image_data) {
-            document.getElementById('patternPreviewImage').src = data.image_data;
-        }
-        
-        // Set pattern name in the preview panel
-        const patternName = pattern.replace('.thr', '').split('/').pop();
-        document.getElementById('patternPreviewTitle').textContent = patternName;
-
-        // Format and display coordinates
-        const formatCoordinate = (coord) => {
-            if (!coord) return '(0, 0)';
-            const x = coord.x !== undefined ? coord.x.toFixed(1) : '0.0';
-            const y = coord.y !== undefined ? coord.y.toFixed(1) : '0.0';
-            return `(${x}, ${y})`;
-        };
-
-        document.getElementById('firstCoordinate').textContent = formatCoordinate(data.first_coordinate);
-        document.getElementById('lastCoordinate').textContent = formatCoordinate(data.last_coordinate);
-        
-        // Show preview panel
-        previewPanel.classList.remove('translate-x-full');
-        if (window.innerWidth >= 1024) {
-            // For large screens, show preview alongside content
-            layoutContainer.parentElement.classList.add('preview-open');
-            previewPanel.classList.remove('lg:opacity-0', 'lg:pointer-events-none');
-        } else {
-            // For small screens, show preview as overlay
-            layoutContainer.parentElement.classList.remove('preview-open');
-        }
-
-        // Setup preview panel events
-        setupPreviewPanelEvents(pattern);
-    } catch (error) {
-        logMessage(`Error showing preview for ${pattern}: ${error.message}`, LOG_TYPE.ERROR);
-        
-        // Show error state in preview panel instead of hiding it
-        showPreviewError(pattern, error.message);
-    }
-}
-
-function showPreviewError(pattern, errorMessage) {
-    const previewPanel = document.getElementById('patternPreviewPanel');
-    const layoutContainer = document.querySelector('.layout-content-container');
-    
-    // Show error state in preview panel
-    const patternName = pattern.replace('.thr', '').split('/').pop();
-    document.getElementById('patternPreviewTitle').textContent = `Error: ${patternName}`;
-    
-    // Show error image or placeholder
-    const img = document.getElementById('patternPreviewImage');
-    img.src = 'data:image/svg+xml;base64,' + btoa(`
-        <svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
-            <rect width="100%" height="100%" fill="#f3f4f6"/>
-            <text x="50%" y="40%" text-anchor="middle" font-family="Arial, sans-serif" font-size="16" fill="#6b7280">
-                Pattern Not Found
-            </text>
-            <text x="50%" y="60%" text-anchor="middle" font-family="Arial, sans-serif" font-size="12" fill="#9ca3af">
-                ${patternName}
-            </text>
-            <text x="50%" y="75%" text-anchor="middle" font-family="Arial, sans-serif" font-size="10" fill="#ef4444">
-                File may have been deleted
-            </text>
-        </svg>
-    `);
-    
-    // Clear coordinates
-    document.getElementById('firstCoordinate').textContent = '(0, 0)';
-    document.getElementById('lastCoordinate').textContent = '(0, 0)';
-    
-    // Show preview panel with error state
-    previewPanel.classList.remove('translate-x-full');
-    if (window.innerWidth >= 1024) {
-        layoutContainer.parentElement.classList.add('preview-open');
-        previewPanel.classList.remove('lg:opacity-0', 'lg:pointer-events-none');
-    } else {
-        layoutContainer.parentElement.classList.remove('preview-open');
-    }
-    
-    // Setup events so user can still close the panel
-    setupPreviewPanelEvents(pattern);
-}
-
-function hidePatternPreview() {
-    const previewPanel = document.getElementById('patternPreviewPanel');
-    const layoutContainer = document.querySelector('.layout-content-container');
-    
-    previewPanel.classList.add('translate-x-full');
-    if (window.innerWidth >= 1024) {
-        previewPanel.classList.add('lg:opacity-0', 'lg:pointer-events-none');
-    }
-    layoutContainer.parentElement.classList.remove('preview-open');
-}
-
-// Add window resize handler
-window.addEventListener('resize', () => {
-    const previewPanel = document.getElementById('patternPreviewPanel');
-    const layoutContainer = document.querySelector('.layout-content-container');
-    
-    if (window.innerWidth >= 1024) {
-        if (!previewPanel.classList.contains('translate-x-full')) {
-            layoutContainer.parentElement.classList.add('preview-open');
-            previewPanel.classList.remove('lg:opacity-0', 'lg:pointer-events-none');
-        }
-    } else {
-        layoutContainer.parentElement.classList.remove('preview-open');
-        previewPanel.classList.add('lg:opacity-0', 'lg:pointer-events-none');
-    }
-    
-    // Update category filter display names for new screen size
-    updateBrowseCategoryFilter();
-});
-
-// Setup preview panel events
-function setupPreviewPanelEvents(pattern) {
-    const panel = document.getElementById('patternPreviewPanel');
-    const closeButton = document.getElementById('closePreviewPanel');
-    const playButton = document.getElementById('playPattern');
-    const deleteButton = document.getElementById('deletePattern');
-    const preExecutionInputs = document.querySelectorAll('input[name="preExecutionAction"]');
-    const previewPlayOverlay = document.getElementById('previewPlayOverlay');
-
-    // Close panel when clicking the close button
-    closeButton.onclick = () => {
-        hidePatternPreview();
-        // Remove selected state from all cards when closing
-        document.querySelectorAll('.pattern-card').forEach(c => {
-            c.classList.remove('selected');
-        });
-    };
-
-    // Handle play button overlay click in preview panel
-    if (previewPlayOverlay) {
-        previewPlayOverlay.onclick = () => {
-            openAnimatedPreview(pattern);
-        };
-    }
-
-    // Handle play button click
-    playButton.onclick = async () => {
-        if (!pattern) {
-            showStatusMessage('No pattern selected', 'error');
-            return;
-        }
-
-        try {
-            // Show the preview modal
-            if (window.openPlayerPreviewModal) {
-                window.openPlayerPreviewModal();
-            }
-
-            // Check if a pattern is currently running and show stopping message
-            console.log('Play clicked, currentPlaybackStatus:', window.currentPlaybackStatus);
-            if (window.currentPlaybackStatus?.is_running) {
-                showStatusMessage('Stopping current pattern...', 'info');
-            }
-
-            // Get the selected pre-execution action
-            const preExecutionInput = document.querySelector('input[name="preExecutionAction"]:checked');
-            const preExecution = preExecutionInput ? preExecutionInput.value : 'none';
-
-            const response = await fetch('/run_theta_rho', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json'
-                },
-                body: JSON.stringify({
-                    file_name: pattern,
-                    pre_execution: preExecution
-                })
-            });
-
-            const data = await response.json();
-            if (response.ok) {
-                showStatusMessage(`Running pattern: ${pattern.split('/').pop()}`, 'success');
-                hidePatternPreview();
-                // Show the preview modal when a pattern starts
-                if (typeof setModalVisibility === 'function') {
-                    setModalVisibility(true, false);
-                }
-                
-            } else {
-                let errorMsg = data.detail || 'Failed to run pattern';
-                let errorType = 'error';
-                
-                // Handle specific error cases with appropriate messaging
-                if (data.detail === 'Connection not established') {
-                    errorMsg = 'Please connect to the device before running a pattern';
-                    errorType = 'warning';
-                } else if (response.status === 409) {
-                    errorMsg = 'Another pattern is already running. Please stop the current pattern first.';
-                    errorType = 'warning';
-                } else if (response.status === 404) {
-                    errorMsg = 'Pattern file not found. Please refresh the page and try again.';
-                    errorType = 'error';
-                } else if (response.status === 400) {
-                    errorMsg = 'Invalid request. Please check your settings and try again.';
-                    errorType = 'error';
-                } else if (response.status === 500) {
-                    errorMsg = 'Server error. Please try again later.';
-                    errorType = 'error';
-                }
-                
-                showStatusMessage(errorMsg, errorType);
-                return;
-            }
-        } catch (error) {
-            console.error('Error running pattern:', error);
-            
-            // Handle network errors specifically
-            if (error.name === 'TypeError' && error.message.includes('fetch')) {
-                showStatusMessage('Network error. Please check your connection and try again.', 'error');
-            } else if (error.message && error.message.includes('409')) {
-                showStatusMessage('Another pattern is already running', 'warning');
-            } else if (error.message) {
-                showStatusMessage(error.message, 'error');
-            } else {
-                showStatusMessage('Failed to run pattern', 'error');
-            }
-        }
-    };
-
-    // Handle delete button click
-    deleteButton.onclick = async () => {
-        if (!pattern.startsWith('custom_patterns/')) {
-            logMessage('Cannot delete built-in patterns', LOG_TYPE.WARNING);
-            showStatusMessage('Cannot delete built-in patterns', 'warning');
-            return;
-        }
-
-        if (confirm('Are you sure you want to delete this pattern?')) {
-            try {
-                logMessage(`Deleting pattern: ${pattern}`, LOG_TYPE.INFO);
-                const response = await fetch('/delete_theta_rho_file', {
-                    method: 'POST',
-                    headers: {
-                        'Content-Type': 'application/json'
-                    },
-                    body: JSON.stringify({ file_name: pattern })
-                });
-
-                if (!response.ok) {
-                    throw new Error(`HTTP error! status: ${response.status}`);
-                }
-
-                const result = await response.json();
-                if (result.success) {
-                    logMessage(`Pattern deleted successfully: ${pattern}`, LOG_TYPE.SUCCESS);
-                    showStatusMessage(`Pattern "${pattern.split('/').pop()}" deleted successfully`);
-
-                    // Invalidate pattern files cache
-                    invalidatePatternFilesCache();
-
-                    // Clear from in-memory caches
-                    previewCache.delete(pattern);
-                    imageCache.delete(pattern);
-                    
-                    // Clear from IndexedDB cache
-                    await clearPatternFromIndexedDB(pattern);
-                    
-                    // Clear from localStorage patterns list cache
-                    const cachedPatterns = JSON.parse(localStorage.getItem(PATTERNS_CACHE_KEY) || '{}');
-                    if (cachedPatterns.data) {
-                        const index = cachedPatterns.data.indexOf(pattern);
-                        if (index > -1) {
-                            cachedPatterns.data.splice(index, 1);
-                            localStorage.setItem(PATTERNS_CACHE_KEY, JSON.stringify(cachedPatterns));
-                        }
-                    }
-                    
-                    // Remove the pattern card
-                    const selectedCard = document.querySelector('.pattern-card.selected');
-                    if (selectedCard) {
-                        selectedCard.remove();
-                    }
-                    // Close the preview panel
-                    const previewPanel = document.getElementById('patternPreviewPanel');
-                    const layoutContainer = document.querySelector('.layout-content-container');
-                    previewPanel.classList.add('translate-x-full');
-                    if (window.innerWidth >= 1024) {
-                        previewPanel.classList.add('lg:opacity-0', 'lg:pointer-events-none');
-                    }
-                    layoutContainer.parentElement.classList.remove('preview-open');
-                    // Clear the preview panel content
-                    document.getElementById('patternPreviewImage').src = '';
-                    document.getElementById('patternPreviewTitle').textContent = 'Pattern Details';
-                    document.getElementById('firstCoordinate').textContent = '(0, 0)';
-                    document.getElementById('lastCoordinate').textContent = '(0, 0)';
-                    // Refresh the pattern list (cache already invalidated above)
-                    await loadPatterns();
-                } else {
-                    throw new Error(result.error || 'Unknown error');
-                }
-            } catch (error) {
-                logMessage(`Failed to delete pattern: ${error.message}`, LOG_TYPE.ERROR);
-                showStatusMessage(`Failed to delete pattern: ${error.message}`, 'error');
-            }
-        }
-    };
-
-    // Handle pre-execution action changes
-    preExecutionInputs.forEach(input => {
-        input.onchange = () => {
-            const action = input.parentElement.textContent.trim();
-            logMessage(`Pre-execution action changed to: ${action}`, LOG_TYPE.INFO);
-        };
-    });
-}
-
-// Search patterns
-// Sort patterns by specified field and direction
-function sortPatterns(patterns, sortField, sortDirection) {
-    return patterns.sort((a, b) => {
-        let aVal, bVal;
-        
-        switch (sortField) {
-            case 'name':
-                aVal = a.name.toLowerCase();
-                bVal = b.name.toLowerCase();
-                break;
-            case 'date':
-                aVal = a.date_modified;
-                bVal = b.date_modified;
-                break;
-            case 'coordinates':
-                aVal = a.coordinates_count;
-                bVal = b.coordinates_count;
-                break;
-            case 'favorite':
-                // Sort by favorite status first, then by name as secondary sort
-                const aIsFavorite = favoritePatterns.has(a.path);
-                const bIsFavorite = favoritePatterns.has(b.path);
-                
-                if (aIsFavorite && !bIsFavorite) return sortDirection === 'asc' ? -1 : 1;
-                if (!aIsFavorite && bIsFavorite) return sortDirection === 'asc' ? 1 : -1;
-                
-                // Both have same favorite status, sort by name as secondary sort
-                aVal = a.name.toLowerCase();
-                bVal = b.name.toLowerCase();
-                break;
-            default:
-                aVal = a.name.toLowerCase();
-                bVal = b.name.toLowerCase();
-        }
-        
-        let result = 0;
-        if (aVal < bVal) result = -1;
-        else if (aVal > bVal) result = 1;
-        
-        return sortDirection === 'asc' ? result : -result;
-    });
-}
-
-// Filter patterns based on current filters
-function filterPatterns(patterns, filters, searchQuery = '') {
-    return patterns.filter(pattern => {
-        // Category filter
-        if (filters.category !== 'all' && pattern.category !== filters.category) {
-            return false;
-        }
-        
-        // Search query filter
-        if (searchQuery.trim()) {
-            const normalizedQuery = searchQuery.toLowerCase().trim();
-            const patternName = pattern.name.toLowerCase();
-            const category = pattern.category.toLowerCase();
-            return patternName.includes(normalizedQuery) || category.includes(normalizedQuery);
-        }
-        
-        return true;
-    });
-}
-
-// Apply sorting and filtering to patterns
-function applyPatternsFilteringAndSorting() {
-    const searchQuery = document.getElementById('patternSearch')?.value || '';
-    
-    // Check if enhanced metadata is available
-    if (!allPatternsWithMetadata || allPatternsWithMetadata.length === 0) {
-        // Fallback to basic search if metadata not loaded yet
-        if (searchQuery.trim()) {
-            const filteredPatterns = allPatterns.filter(pattern => 
-                pattern.toLowerCase().includes(searchQuery.toLowerCase())
-            );
-            displayFilteredPatterns(filteredPatterns);
-        } else {
-            // Just display current batch if no search
-            displayPatternBatch();
-        }
-        return;
-    }
-    
-    // Start with all available patterns with metadata
-    let patterns = [...allPatternsWithMetadata];
-    
-    // Apply filters
-    patterns = filterPatterns(patterns, currentFilters, searchQuery);
-    
-    // Apply sorting
-    patterns = sortPatterns(patterns, currentSort.field, currentSort.direction);
-    
-    // Update filtered patterns (convert back to path format for compatibility)
-    const filteredPatterns = patterns.map(p => p.path);
-    
-    // Display filtered patterns
-    displayFilteredPatterns(filteredPatterns);
-    updateBrowseSortAndFilterUI();
-}
-
-// Display filtered patterns
-function displayFilteredPatterns(filteredPatterns) {
-    const patternGrid = document.querySelector('.grid');
-    if (!patternGrid) return;
-    
-    patternGrid.innerHTML = '';
-    
-    if (filteredPatterns.length === 0) {
-        patternGrid.innerHTML = '<div class="col-span-full text-center text-gray-500 py-8">No patterns found</div>';
-        return;
-    }
-    
-    filteredPatterns.forEach(pattern => {
-        const patternCard = createPatternCard(pattern);
-        patternGrid.appendChild(patternCard);
-    });
-    
-    // Give the browser a chance to render the cards
-    requestAnimationFrame(() => {
-        // Trigger preview loading for the search results
-        triggerPreviewLoadingForVisible();
-    });
-    
-    logMessage(`Displaying ${filteredPatterns.length} patterns`, LOG_TYPE.INFO);
-}
-
-function searchPatterns(query) {
-    // Update the search input if called programmatically
-    const searchInput = document.getElementById('patternSearch');
-    if (searchInput && searchInput.value !== query) {
-        searchInput.value = query;
-    }
-    
-    applyPatternsFilteringAndSorting();
-}
-
-// Update sort and filter UI to reflect current state
-function updateBrowseSortAndFilterUI() {
-    // Update sort direction icon
-    const sortDirectionIcon = document.getElementById('browseSortDirectionIcon');
-    if (sortDirectionIcon) {
-        sortDirectionIcon.textContent = currentSort.direction === 'asc' ? 'arrow_upward' : 'arrow_downward';
-    }
-    
-    // Update sort field select
-    const sortFieldSelect = document.getElementById('browseSortFieldSelect');
-    if (sortFieldSelect) {
-        sortFieldSelect.value = currentSort.field;
-    }
-    
-    // Update filter selects
-    const categorySelect = document.getElementById('browseCategoryFilterSelect');
-    if (categorySelect) {
-        categorySelect.value = currentFilters.category;
-    }
-}
-
-// Populate category filter dropdown with available categories (subfolders)
-function updateBrowseCategoryFilter() {
-    const categorySelect = document.getElementById('browseCategoryFilterSelect');
-    if (!categorySelect) return;
-    
-    // Check if metadata is available
-    if (!allPatternsWithMetadata || allPatternsWithMetadata.length === 0) {
-        // Show basic options if metadata not loaded
-        categorySelect.innerHTML = '<option value="all">All Folders (loading...)</option>';
-        return;
-    }
-    
-    // Get unique categories (subfolders)
-    const categories = [...new Set(allPatternsWithMetadata.map(p => p.category))].sort();
-    
-    // Clear existing options except "All"
-    categorySelect.innerHTML = '<option value="all">All Folders</option>';
-    
-    // Add category options
-    categories.forEach(category => {
-        if (category) {
-            const option = document.createElement('option');
-            option.value = category;
-            // Display friendly names for full paths
-            if (category === 'root') {
-                option.textContent = 'Root Folder';
-            } else {
-                // For full paths, show the path but make it more readable
-                const parts = category
-                    .split('/')
-                    .map(part => part.charAt(0).toUpperCase() + part.slice(1).replace('_', ' '));
-                
-                // Check if we're on a small screen and truncate if necessary
-                const isSmallScreen = window.innerWidth < 640; // sm breakpoint
-                let displayName;
-                
-                if (isSmallScreen && parts.length > 1) {
-                    // On small screens, show only the last part with "..." if nested
-                    displayName = '...' + parts[parts.length - 1];
-                } else {
-                    // Full path with separators
-                    displayName = parts.join(' › ');
-                }
-                
-                option.textContent = displayName;
-            }
-            categorySelect.appendChild(option);
-        }
-    });
-}
-
-// Handle sort field change
-function handleBrowseSortFieldChange() {
-    const sortFieldSelect = document.getElementById('browseSortFieldSelect');
-    if (sortFieldSelect) {
-        currentSort.field = sortFieldSelect.value;
-        applyPatternsFilteringAndSorting();
-    }
-}
-
-// Handle sort direction toggle
-function handleBrowseSortDirectionToggle() {
-    currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
-    applyPatternsFilteringAndSorting();
-}
-
-// Handle category filter change
-function handleBrowseCategoryFilterChange() {
-    const categorySelect = document.getElementById('browseCategoryFilterSelect');
-    if (categorySelect) {
-        currentFilters.category = categorySelect.value;
-        applyPatternsFilteringAndSorting();
-    }
-}
-
-// Enable sort controls when metadata is loaded
-function enableSortControls() {
-    const browseSortFieldSelect = document.getElementById('browseSortFieldSelect');
-    const browseSortDirectionBtn = document.getElementById('browseSortDirectionBtn');
-    const browseCategoryFilterSelect = document.getElementById('browseCategoryFilterSelect');
-    
-    if (browseSortFieldSelect) {
-        browseSortFieldSelect.disabled = false;
-        // Ensure dropdown shows the current sort field
-        browseSortFieldSelect.value = currentSort.field;
-    }
-    
-    if (browseSortDirectionBtn) {
-        browseSortDirectionBtn.disabled = false;
-        browseSortDirectionBtn.classList.remove('opacity-50', 'cursor-not-allowed');
-        browseSortDirectionBtn.classList.add('hover:bg-gray-200');
-        browseSortDirectionBtn.title = 'Toggle sort direction';
-        
-        // Update direction icon
-        const sortDirectionIcon = document.getElementById('browseSortDirectionIcon');
-        if (sortDirectionIcon) {
-            sortDirectionIcon.textContent = currentSort.direction === 'asc' ? 'arrow_upward' : 'arrow_downward';
-        }
-    }
-    
-    if (browseCategoryFilterSelect) {
-        browseCategoryFilterSelect.disabled = false;
-    }
-    
-    // Only apply sorting if user has changed from defaults or if patterns need to be refreshed
-    // If already showing patterns with default sort (name, asc), don't reorder unnecessarily
-    if (currentSort.field !== 'name' || currentSort.direction !== 'asc' || currentFilters.category !== 'all') {
-        applyPatternsFilteringAndSorting();
-    }
-}
-
-// Filter patterns by category
-function filterPatternsByCategory(category) {
-    // TODO: Implement category filtering logic
-    logMessage(`Filtering patterns by category: ${category}`, LOG_TYPE.INFO);
-}
-
-// Filter patterns by tag
-function filterPatternsByTag(tag) {
-    // TODO: Implement tag filtering logic
-    logMessage(`Filtering patterns by tag: ${tag}`, LOG_TYPE.INFO);
-}
-
-// Initialize the patterns page
-document.addEventListener('DOMContentLoaded', async () => {
-    try {
-        logMessage('Initializing patterns page...', LOG_TYPE.DEBUG);
-        
-        // Initialize IndexedDB preview cache (shared with playlists page)
-        await initPreviewCacheDB();
-        
-        // Setup upload event handlers
-        setupUploadEventHandlers();
-        
-        // Initialize intersection observer for lazy loading
-        initPreviewObserver();
-
-        // Setup search functionality
-        const searchInput = document.getElementById('patternSearch');
-        const searchButton = document.getElementById('searchButton');
-        const cacheAllButton = document.getElementById('cacheAllButton');
-        
-        if (searchInput && searchButton) {
-            // Search on button click
-            searchButton.addEventListener('click', () => {
-                searchPatterns(searchInput.value.trim());
-            });
-            
-            // Search on Enter key
-            searchInput.addEventListener('keypress', (e) => {
-                if (e.key === 'Enter') {
-                    searchPatterns(searchInput.value.trim());
-                }
-            });
-            
-            // Clear search when input is empty
-            searchInput.addEventListener('input', (e) => {
-                if (e.target.value.trim() === '') {
-                    searchPatterns('');
-                }
-            });
-        }
-        
-        // Sort and filter controls for browse page
-        const browseSortFieldSelect = document.getElementById('browseSortFieldSelect');
-        const browseSortDirectionBtn = document.getElementById('browseSortDirectionBtn');
-        const browseCategoryFilterSelect = document.getElementById('browseCategoryFilterSelect');
-        
-        if (browseSortFieldSelect) {
-            browseSortFieldSelect.addEventListener('change', handleBrowseSortFieldChange);
-        }
-        if (browseSortDirectionBtn) {
-            browseSortDirectionBtn.addEventListener('click', handleBrowseSortDirectionToggle);
-        }
-        if (browseCategoryFilterSelect) {
-            browseCategoryFilterSelect.addEventListener('change', handleBrowseCategoryFilterChange);
-        }
-
-        // Setup cache all button - now triggers the modal
-        if (cacheAllButton) {
-            cacheAllButton.addEventListener('click', () => {
-                // Always show the modal when manually clicked, using forceShow parameter
-                if (typeof showCacheAllPrompt === 'function') {
-                    showCacheAllPrompt(true); // true = forceShow
-                } else {
-                    // Fallback if function not available
-                    const modal = document.getElementById('cacheAllPromptModal');
-                    if (modal) {
-                        modal.classList.remove('hidden');
-                        modal.dataset.manuallyTriggered = 'true';
-                    }
-                }
-            });
-        }
-
-        // Load favorites first, then patterns
-        await loadFavorites();
-        await loadPatterns();
-        
-        logMessage('Patterns page initialized successfully', LOG_TYPE.SUCCESS);
-    } catch (error) {
-        logMessage(`Error during initialization: ${error.message}`, LOG_TYPE.ERROR);
-    }
-});
-
-// Cancel any pending requests when navigating away from the page
-window.addEventListener('beforeunload', () => {
-    if (metadataAbortController) {
-        metadataAbortController.abort();
-        metadataAbortController = null;
-        logMessage('Cancelled pending metadata request due to navigation', LOG_TYPE.DEBUG);
-    }
-});
-
-// Also handle page visibility changes (switching tabs, minimizing, etc.)
-document.addEventListener('visibilitychange', () => {
-    if (document.hidden && metadataAbortController) {
-        // Cancel long-running requests when page is hidden
-        metadataAbortController.abort();
-        metadataAbortController = null;
-        logMessage('Cancelled pending metadata request due to page hidden', LOG_TYPE.DEBUG);
-    }
-});
-
-function updateCurrentlyPlayingUI(status) {
-    // Get all required DOM elements once
-    const container = document.getElementById('currently-playing-container');
-    const fileNameElement = document.getElementById('currently-playing-file');
-    const progressBar = document.getElementById('play_progress');
-    const progressText = document.getElementById('play_progress_text');
-    const pausePlayButton = document.getElementById('pausePlayCurrent');
-    const speedDisplay = document.getElementById('current_speed_display');
-    const speedInput = document.getElementById('speedInput');
-
-    // Check if all required elements exist
-    if (!container || !fileNameElement || !progressBar || !progressText) {
-        console.log('Required DOM elements not found:', {
-            container: !!container,
-            fileNameElement: !!fileNameElement,
-            progressBar: !!progressBar,
-            progressText: !!progressText
-        });
-        setTimeout(() => updateCurrentlyPlayingUI(status), 100);
-        return;
-    }
-
-    // Update container visibility based on status
-    if (status.current_file && status.is_running) {
-        document.body.classList.add('playing');
-        container.style.display = 'flex';
-    } else {
-        document.body.classList.remove('playing');
-        container.style.display = 'none';
-    }
-
-    // Update file name display
-    if (status.current_file) {
-        const fileName = normalizeFilePath(status.current_file);
-        fileNameElement.textContent = fileName;
-    } else {
-        fileNameElement.textContent = 'No pattern playing';
-    }
-
-    // Update next file display
-    const nextFileElement = document.getElementById('next-file');
-    if (nextFileElement) {
-        if (status.playlist && status.playlist.next_file) {
-            const nextFileName = normalizeFilePath(status.playlist.next_file);
-            nextFileElement.textContent = `(Next: ${nextFileName})`;
-            nextFileElement.style.display = 'block';
-        } else {
-            nextFileElement.style.display = 'none';
-        }
-    }
-
-    // Update speed display and input if they exist
-    if (status.speed) {
-        if (speedDisplay) {
-            speedDisplay.textContent = `Current Speed: ${status.speed}`;
-        }
-        if (speedInput) {
-            speedInput.value = status.speed;
-        }
-    }
-
-    // Update pattern preview if it's a new pattern
-    // ... existing code ...
-}
-
-// Setup upload event handlers
-function setupUploadEventHandlers() {
-    // Upload file input handler - supports multiple files
-    document.getElementById('patternFileInput').addEventListener('change', async function(e) {
-        const files = e.target.files;
-        if (!files || files.length === 0) return;
-
-        const totalFiles = files.length;
-        const fileArray = Array.from(files);
-        let successCount = 0;
-        let failCount = 0;
-        
-        // Show initial progress message
-        showStatusMessage(`Uploading ${totalFiles} pattern${totalFiles > 1 ? 's' : ''}...`);
-
-        try {
-            // Upload files sequentially to avoid overwhelming the server
-            for (let i = 0; i < fileArray.length; i++) {
-                const file = fileArray[i];
-                
-                try {
-                    const formData = new FormData();
-                    formData.append('file', file);
-
-                    const response = await fetch('/upload_theta_rho', {
-                        method: 'POST',
-                        body: formData
-                    });
-
-                    const result = await response.json();
-                    if (result.success) {
-                        successCount++;
-
-                        // Invalidate pattern files cache to include new file
-                        invalidatePatternFilesCache();
-
-                        // Clear any existing cache for this pattern to ensure fresh loading
-                        const newPatternPath = `custom_patterns/${file.name}`;
-                        previewCache.delete(newPatternPath);
-
-                        logMessage(`Successfully uploaded: ${file.name}`, LOG_TYPE.SUCCESS);
-                    } else {
-                        failCount++;
-                        logMessage(`Failed to upload ${file.name}: ${result.error}`, LOG_TYPE.ERROR);
-                    }
-                } catch (fileError) {
-                    failCount++;
-                    logMessage(`Error uploading ${file.name}: ${fileError.message}`, LOG_TYPE.ERROR);
-                }
-                
-                // Update progress
-                const progress = i + 1;
-                showStatusMessage(`Uploading patterns... ${progress}/${totalFiles}`);
-            }
-            
-            // Show final result
-            if (successCount > 0) {
-                const message = failCount > 0 
-                    ? `Uploaded ${successCount} pattern${successCount > 1 ? 's' : ''}, ${failCount} failed`
-                    : `Successfully uploaded ${successCount} pattern${successCount > 1 ? 's' : ''}`;
-                showStatusMessage(message);
-                
-                // Add a small delay to allow backend preview generation to complete
-                await new Promise(resolve => setTimeout(resolve, 1000));
-                
-                // Refresh the pattern list (cache already invalidated above)
-                await loadPatterns();
-                
-                // Trigger preview loading for newly uploaded patterns
-                setTimeout(() => {
-                    fileArray.forEach(file => {
-                        const newPatternPath = `custom_patterns/${file.name}`;
-                        const newPatternCard = document.querySelector(`[data-pattern="${newPatternPath}"]`);
-                        if (newPatternCard) {
-                            const previewContainer = newPatternCard.querySelector('.pattern-preview');
-                            if (previewContainer) {
-                                previewContainer.dataset.retryCount = '0';
-                                previewContainer.dataset.hasTriedIndividual = 'false';
-                                previewContainer.dataset.isNewUpload = 'true';
-                                addPatternToBatch(newPatternPath, previewContainer);
-                            }
-                        }
-                    });
-                }, 500);
-            } else {
-                showStatusMessage(`Failed to upload all ${totalFiles} pattern${totalFiles > 1 ? 's' : ''}`, 'error');
-            }
-            
-            // Clear the file input
-            e.target.value = '';
-            
-        } catch (error) {
-            console.error('Error during batch upload:', error);
-            showStatusMessage(`Error uploading patterns: ${error.message}`, 'error');
-        }
-    });
-
-    // Pattern deletion handler
-    const deleteModal = document.getElementById('deleteConfirmModal');
-    if (deleteModal) {
-        const confirmBtn = deleteModal.querySelector('#confirmDeleteBtn');
-        const cancelBtn = deleteModal.querySelector('#cancelDeleteBtn');
-        
-        if (confirmBtn) {
-            confirmBtn.addEventListener('click', async () => {
-                const patternToDelete = confirmBtn.dataset.pattern;
-                if (patternToDelete) {
-                    await deletePattern(patternToDelete);
-                    // Refresh after deletion (cache invalidated in deletePattern)
-                    await loadPatterns();
-                }
-                deleteModal.classList.add('hidden');
-            });
-        }
-        
-        if (cancelBtn) {
-            cancelBtn.addEventListener('click', () => {
-                deleteModal.classList.add('hidden');
-            });
-        }
-    }
-}
-
-// Cache all pattern previews
-async function cacheAllPreviews() {
-    const cacheAllButton = document.getElementById('cacheAllButton');
-    if (!cacheAllButton) return;
-
-    try {
-        // Disable button and show loading state
-        cacheAllButton.disabled = true;
-
-        // Get current cache size
-        const currentSize = await getPreviewCacheSize();
-        const maxSize = MAX_CACHE_SIZE_BYTES || (200 * 1024 * 1024); // 200MB default
-
-        if (currentSize > maxSize) {
-            // Clear cache if it's too large
-            await clearPreviewCache();
-            // Also clear progress since we're starting fresh
-            localStorage.removeItem(CACHE_PROGRESS_KEY);
-            localStorage.removeItem(CACHE_TIMESTAMP_KEY);
-        }
-
-        // Get all patterns that aren't cached yet
-        const uncachedPatterns = allPatterns.filter(pattern => !previewCache.has(pattern));
-        
-        if (uncachedPatterns.length === 0) {
-            showStatusMessage('All patterns are already cached!', 'info');
-            return;
-        }
-
-        // Check for existing progress
-        let startIndex = 0;
-        const savedProgress = localStorage.getItem(CACHE_PROGRESS_KEY);
-        const savedTimestamp = localStorage.getItem(CACHE_TIMESTAMP_KEY);
-        
-        if (savedProgress && savedTimestamp) {
-            const progressAge = Date.now() - parseInt(savedTimestamp);
-            if (progressAge < CACHE_PROGRESS_EXPIRY) {
-                const lastCachedPattern = savedProgress;
-                const lastIndex = uncachedPatterns.findIndex(p => p === lastCachedPattern);
-                if (lastIndex !== -1) {
-                    startIndex = lastIndex + 1;
-                    showStatusMessage('Resuming from previous progress...', 'info');
-                }
-            } else {
-                // Clear expired progress
-                localStorage.removeItem(CACHE_PROGRESS_KEY);
-                localStorage.removeItem(CACHE_TIMESTAMP_KEY);
-            }
-        }
-
-        // Process patterns in smaller batches to avoid overwhelming the server
-        const BATCH_SIZE = 10;
-        const remainingPatterns = uncachedPatterns.slice(startIndex);
-        const totalBatches = Math.ceil(remainingPatterns.length / BATCH_SIZE);
-        
-        for (let i = 0; i < totalBatches; i++) {
-            const batchStart = i * BATCH_SIZE;
-            const batchEnd = Math.min(batchStart + BATCH_SIZE, remainingPatterns.length);
-            const batchPatterns = remainingPatterns.slice(batchStart, batchEnd);
-            
-            // Update button text with progress
-            const overallProgress = Math.round(((startIndex + batchStart + BATCH_SIZE) / uncachedPatterns.length) * 100);
-            cacheAllButton.innerHTML = `
-                <div class="bg-white bg-opacity-30 rounded-full h-4 w-4 flex items-center justify-center">
-                    <div class="bg-white rounded-full h-2 w-2"></div>
-                </div>
-                <span>Caching ${overallProgress}%</span>
-            `;
-
-            try {
-                const response = await fetch('/preview_thr_batch', {
-                    method: 'POST',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({ file_names: batchPatterns })
-                });
-
-                if (response.ok) {
-                    const results = await response.json();
-                    
-                    // Cache each preview
-                    for (const [pattern, data] of Object.entries(results)) {
-                        if (data && !data.error && data.image_data) {
-                            previewCache.set(pattern, data);
-                            await savePreviewToCache(pattern, data);
-                            
-                            // Save progress after each successful pattern
-                            localStorage.setItem(CACHE_PROGRESS_KEY, pattern);
-                            localStorage.setItem(CACHE_TIMESTAMP_KEY, Date.now().toString());
-                        }
-                    }
-                }
-            } catch (error) {
-                logMessage(`Error caching batch ${i + 1}: ${error.message}`, LOG_TYPE.ERROR);
-                // Don't clear progress on error - allows resuming from last successful pattern
-            }
-
-            // Small delay between batches to prevent overwhelming the server
-            await new Promise(resolve => setTimeout(resolve, 100));
-        }
-
-        // Clear progress after successful completion
-        localStorage.removeItem(CACHE_PROGRESS_KEY);
-        localStorage.removeItem(CACHE_TIMESTAMP_KEY);
-
-        // Show success message
-        showStatusMessage('All pattern previews have been cached!', 'success');
-    } catch (error) {
-        logMessage(`Error caching previews: ${error.message}`, LOG_TYPE.ERROR);
-        showStatusMessage('Failed to cache all previews. Click again to resume.', 'error');
-    } finally {
-        // Reset button state
-        if (cacheAllButton) {
-            cacheAllButton.disabled = false;
-            cacheAllButton.innerHTML = `
-                <span class="material-icons text-sm">cached</span>
-                Cache All Previews
-            `;
-        }
-    }
-}
-
-// Open animated preview modal
-async function openAnimatedPreview(pattern) {
-    try {
-        const modal = document.getElementById('animatedPreviewModal');
-        const title = document.getElementById('animatedPreviewTitle');
-        const canvas = document.getElementById('animatedPreviewCanvas');
-        const ctx = canvas.getContext('2d');
-        
-        // Set title
-        title.textContent = pattern.replace('.thr', '').split('/').pop();
-        
-        // Show modal
-        modal.classList.remove('hidden');
-        
-        // Load pattern coordinates
-        const response = await fetch('/get_theta_rho_coordinates', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ file_name: pattern })
-        });
-        
-        if (!response.ok) {
-            throw new Error(`HTTP error! status: ${response.status}`);
-        }
-        
-        const data = await response.json();
-        if (data.error) {
-            throw new Error(data.error);
-        }
-        
-        animatedPreviewData = data.coordinates;
-        
-        // Setup canvas
-        setupAnimatedPreviewCanvas(ctx);
-        
-        // Setup controls
-        setupAnimatedPreviewControls();
-        
-        // Draw initial state
-        drawAnimatedPreview(ctx, 0);
-        
-        // Auto-play the animation
-        setTimeout(() => {
-            playAnimation();
-        }, 100); // Small delay to ensure everything is set up
-        
-    } catch (error) {
-        logMessage(`Error opening animated preview: ${error.message}`, LOG_TYPE.ERROR);
-        showStatusMessage('Failed to load pattern for animation', 'error');
-    }
-}
-
-// Setup animated preview canvas
-function setupAnimatedPreviewCanvas(ctx) {
-    const canvas = ctx.canvas;
-    const size = canvas.width;
-    const center = size / 2;
-    const scale = (size / 2) - 30; // Slightly smaller to account for border
-    
-    // Clear canvas with white background
-    ctx.fillStyle = '#ffffff';
-    ctx.fillRect(0, 0, size, size);
-    
-    // Set drawing style for ultra-high quality lines
-    ctx.strokeStyle = '#000000';
-    ctx.lineWidth = 1; // Thinner line for higher resolution
-    ctx.lineCap = 'round';
-    ctx.lineJoin = 'round';
-    
-    // Enable high quality rendering
-    ctx.imageSmoothingEnabled = true;
-    ctx.imageSmoothingQuality = 'high';
-}
-
-// Setup animated preview controls
-function setupAnimatedPreviewControls() {
-    const modal = document.getElementById('animatedPreviewModal');
-    const closeBtn = document.getElementById('closeAnimatedPreview');
-    const playPauseBtn = document.getElementById('playPauseBtn');
-    const resetBtn = document.getElementById('resetBtn');
-    const speedSlider = document.getElementById('speedSlider');
-    const speedValue = document.getElementById('speedValue');
-    const progressSlider = document.getElementById('progressSlider');
-    const progressValue = document.getElementById('progressValue');
-    const canvas = document.getElementById('animatedPreviewCanvas');
-    const playPauseOverlay = document.getElementById('playPauseOverlay');
-    
-    // Set responsive canvas size with ultra-high-DPI support
-    const setCanvasSize = () => {
-        const container = canvas.parentElement;
-        const modal = document.getElementById('animatedPreviewModal');
-        if (!container || !modal) return;
-        
-        // Calculate available viewport space
-        const viewportWidth = window.innerWidth;
-        const viewportHeight = window.innerHeight;
-        
-        // Calculate modal content area (95vh max height - header - padding)
-        const modalMaxHeight = viewportHeight * 0.95;
-        const headerHeight = 80; // Approximate header height with padding
-        const modalPadding = 48; // Modal padding (p-6 = 24px each side)
-        const availableHeight = modalMaxHeight - headerHeight - modalPadding;
-        
-        // Calculate available width (max-w-4xl = 896px, but respect viewport)
-        const modalMaxWidth = Math.min(896, viewportWidth - 32); // Account for modal margin
-        const availableWidth = modalMaxWidth - modalPadding;
-        
-        // Calculate ideal canvas size (use 80% of available space as requested)
-        const targetHeight = availableHeight * 0.8;
-        const targetWidth = availableWidth * 0.8;
-        
-        // Use the smaller dimension to maintain square aspect ratio
-        let idealSize = Math.min(targetWidth, targetHeight);
-        
-        // Cap at reasonable maximum and minimum
-        idealSize = Math.min(idealSize, 800); // Maximum size cap
-        idealSize = Math.max(idealSize, 200); // Minimum size
-        
-        const displaySize = idealSize;
-        
-        console.log('Canvas sizing:', {
-            viewport: `${viewportWidth}x${viewportHeight}`,
-            availableModal: `${availableWidth}x${availableHeight}`,
-            target80pct: `${targetWidth}x${targetHeight}`,
-            finalSize: displaySize
-        });
-        
-        // Get device pixel ratio and multiply by 2 for higher resolution
-        const pixelRatio = (window.devicePixelRatio || 1) * 2;
-        
-        // Set the display size (CSS pixels) - use pixels, not percentage
-        canvas.style.width = displaySize + 'px';
-        canvas.style.height = displaySize + 'px';
-        
-        // Set the actual canvas size (device pixels) - increased resolution
-        canvas.width = displaySize * pixelRatio;
-        canvas.height = displaySize * pixelRatio;
-        
-        // Scale the context to match the increased pixel ratio
-        const ctx = canvas.getContext('2d', { alpha: false }); // Disable alpha for better performance
-        ctx.scale(pixelRatio, pixelRatio);
-        
-        // Enable high quality rendering
-        ctx.imageSmoothingEnabled = true;
-        ctx.imageSmoothingQuality = 'high';
-        
-        // Redraw with new size
-        if (animatedPreviewData) {
-            setupAnimatedPreviewCanvas(ctx);
-            drawAnimatedPreview(ctx, currentProgress / 100);
-        }
-    };
-    
-    // Set initial size
-    setCanvasSize();
-    
-    // Handle window resize with debouncing
-    let resizeTimeout;
-    window.addEventListener('resize', () => {
-        clearTimeout(resizeTimeout);
-        resizeTimeout = setTimeout(setCanvasSize, 16); // ~60fps update rate
-    });
-    
-    // Close modal
-    closeBtn.onclick = closeAnimatedPreview;
-    modal.onclick = (e) => {
-        if (e.target === modal) closeAnimatedPreview();
-    };
-    
-    // Play/Pause button
-    playPauseBtn.onclick = toggleAnimation;
-    
-    // Reset button
-    resetBtn.onclick = resetAnimation;
-    
-    // Speed slider
-    speedSlider.oninput = (e) => {
-        animationSpeed = parseFloat(e.target.value);
-        speedValue.textContent = `${animationSpeed}x`;
-    };
-    
-    // Progress slider
-    progressSlider.oninput = (e) => {
-        currentProgress = parseFloat(e.target.value);
-        progressValue.textContent = `${currentProgress.toFixed(1)}%`;
-        drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
-        if (isPlaying) {
-            // Pause animation when manually adjusting progress
-            toggleAnimation();
-        }
-    };
-    
-    // Canvas click to play/pause
-    canvas.onclick = () => {
-        playPauseOverlay.style.opacity = '1';
-        setTimeout(() => {
-            playPauseOverlay.style.opacity = '0';
-        }, 200);
-        toggleAnimation();
-    };
-    
-    // Keyboard shortcuts
-    document.addEventListener('keydown', (e) => {
-        if (modal.classList.contains('hidden')) return;
-        
-        switch(e.code) {
-            case 'Space':
-                e.preventDefault();
-                toggleAnimation();
-                break;
-            case 'Escape':
-                closeAnimatedPreview();
-                break;
-            case 'ArrowLeft':
-                e.preventDefault();
-                currentProgress = Math.max(0, currentProgress - 5);
-                updateProgressUI();
-                drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
-                break;
-            case 'ArrowRight':
-                e.preventDefault();
-                currentProgress = Math.min(100, currentProgress + 5);
-                updateProgressUI();
-                drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
-                break;
-        }
-    });
-}
-
-// Draw animated preview
-function drawAnimatedPreview(ctx, progress) {
-    if (!animatedPreviewData || animatedPreviewData.length === 0) return;
-    
-    const canvas = ctx.canvas;
-    const pixelRatio = (window.devicePixelRatio || 1) * 2; // Match the increased ratio
-    const displayWidth = parseInt(canvas.style.width);
-    const displayHeight = parseInt(canvas.style.height);
-    const center = (canvas.width / pixelRatio) / 2;
-    const scale = ((canvas.width / pixelRatio) / 2) - 30;
-    
-    // Clear canvas with white background
-    ctx.clearRect(0, 0, canvas.width, canvas.height);
-    
-    // Calculate how many points to draw
-    const totalPoints = animatedPreviewData.length;
-    const pointsToDraw = Math.floor(totalPoints * progress);
-    
-    if (pointsToDraw < 2) return;
-    
-    // Draw the path with ultra-high quality settings
-    ctx.beginPath();
-    ctx.strokeStyle = '#000000';
-    ctx.lineWidth = 1; // Thinner line for higher resolution
-    ctx.lineCap = 'round';
-    ctx.lineJoin = 'round';
-    
-    // Ensure sub-pixel alignment for ultra-high resolution
-    for (let i = 0; i < pointsToDraw; i++) {
-        const [theta, rho] = animatedPreviewData[i];
-        // Round to nearest 0.25 for even more precise lines
-        // Mirror both X and Y coordinates
-        const x = Math.round((center + rho * scale * Math.cos(theta)) * 4) / 4; // Changed minus to plus
-        const y = Math.round((center + rho * scale * Math.sin(theta)) * 4) / 4;
-        
-        if (i === 0) {
-            ctx.moveTo(x, y);
-        } else {
-            ctx.lineTo(x, y);
-        }
-    }
-    ctx.stroke();
-    
-    // Draw current position dot
-    if (pointsToDraw > 0) {
-        const [currentTheta, currentRho] = animatedPreviewData[pointsToDraw - 1];
-        const currentX = Math.round((center + currentRho * scale * Math.cos(currentTheta)) * 4) / 4; // Changed minus to plus
-        const currentY = Math.round((center + currentRho * scale * Math.sin(currentTheta)) * 4) / 4;
-        
-        // Draw a filled circle at current position with anti-aliasing
-        ctx.fillStyle = '#ff4444'; // Red dot
-        ctx.beginPath();
-        ctx.arc(currentX, currentY, 6, 0, 2 * Math.PI); // Increased dot size
-        ctx.fill();
-        
-        // Add a subtle white border
-        ctx.strokeStyle = '#ffffff';
-        ctx.lineWidth = 1.5;
-        ctx.stroke();
-    }
-}
-
-// Toggle animation play/pause
-function toggleAnimation() {
-    if (isPlaying) {
-        pauseAnimation();
-    } else {
-        playAnimation();
-    }
-}
-
-// Play animation
-function playAnimation() {
-    if (!animatedPreviewData) return;
-    
-    isPlaying = true;
-    lastTimestamp = performance.now();
-    
-    // Update UI
-    const playPauseBtn = document.getElementById('playPauseBtn');
-    const playPauseBtnIcon = document.getElementById('playPauseBtnIcon');
-    const playPauseBtnText = document.getElementById('playPauseBtnText');
-    
-    if (playPauseBtnIcon) playPauseBtnIcon.textContent = 'pause';
-    if (playPauseBtnText) playPauseBtnText.textContent = 'Pause';
-    
-    // Start animation loop
-    animationFrameId = requestAnimationFrame(animate);
-}
-
-// Pause animation
-function pauseAnimation() {
-    isPlaying = false;
-    
-    // Update UI
-    const playPauseBtn = document.getElementById('playPauseBtn');
-    const playPauseBtnIcon = document.getElementById('playPauseBtnIcon');
-    const playPauseBtnText = document.getElementById('playPauseBtnText');
-    
-    if (playPauseBtnIcon) playPauseBtnIcon.textContent = 'play_arrow';
-    if (playPauseBtnText) playPauseBtnText.textContent = 'Play';
-    
-    // Cancel animation frame
-    if (animationFrameId) {
-        cancelAnimationFrame(animationFrameId);
-        animationFrameId = null;
-    }
-}
-
-// Animation loop
-function animate(timestamp) {
-    if (!isPlaying) return;
-    
-    const deltaTime = timestamp - lastTimestamp;
-    const progressIncrement = (deltaTime / 1000) * animationSpeed * 2.0; // Much faster base speed
-    
-    currentProgress = Math.min(100, currentProgress + progressIncrement);
-    
-    // Update UI
-    updateProgressUI();
-    
-    // Draw frame
-    const canvas = document.getElementById('animatedPreviewCanvas');
-    if (canvas) {
-        drawAnimatedPreview(canvas.getContext('2d'), currentProgress / 100);
-    }
-    
-    // Continue animation
-    if (currentProgress < 100) {
-        lastTimestamp = timestamp;
-        animationFrameId = requestAnimationFrame(animate);
-    } else {
-        // Animation complete
-        pauseAnimation();
-    }
-}
-
-// Reset animation
-function resetAnimation() {
-    pauseAnimation();
-    currentProgress = 0;
-    updateProgressUI();
-    
-    const canvas = document.getElementById('animatedPreviewCanvas');
-    drawAnimatedPreview(canvas.getContext('2d'), 0);
-}
-
-// Update progress UI
-function updateProgressUI() {
-    const progressSlider = document.getElementById('progressSlider');
-    const progressValue = document.getElementById('progressValue');
-    
-    progressSlider.value = currentProgress;
-    progressValue.textContent = `${currentProgress.toFixed(1)}%`;
-}
-
-// Close animated preview
-function closeAnimatedPreview() {
-    pauseAnimation();
-    
-    const modal = document.getElementById('animatedPreviewModal');
-    modal.classList.add('hidden');
-    
-    // Clear data
-    animatedPreviewData = null;
-    currentProgress = 0;
-    animationSpeed = 1;
-    
-    // Reset UI
-    const speedSlider = document.getElementById('speedSlider');
-    const speedValue = document.getElementById('speedValue');
-    const progressSlider = document.getElementById('progressSlider');
-    const progressValue = document.getElementById('progressValue');
-    
-    speedSlider.value = 1;
-    speedValue.textContent = '1x';
-    progressSlider.value = 0;
-    progressValue.textContent = '0%';
-}
-
-// Global set to track favorite patterns
-let favoritePatterns = new Set();
-// Make favoritePatterns available globally for other scripts
-window.favoritePatterns = favoritePatterns;
-
-// Load favorites from server on page load
-async function loadFavorites() {
-    try {
-        const response = await fetch('/get_playlist?name=Favorites');
-        if (response.ok) {
-            const playlist = await response.json();
-            favoritePatterns = new Set(playlist.files);
-            window.favoritePatterns = favoritePatterns; // Keep window reference updated
-            updateAllHeartIcons();
-        }
-    } catch (error) {
-        // Favorites playlist doesn't exist yet - that's OK
-        console.debug('Favorites playlist not found, will create when needed');
-    }
-}
-
-// Toggle favorite status
-async function toggleFavorite(pattern) {
-    const heartIcon = document.getElementById('heart-' + pattern.replace(/[^a-zA-Z0-9]/g, '_'));
-    if (!heartIcon) return;
-    
-    try {
-        if (favoritePatterns.has(pattern)) {
-            // Remove from favorites
-            await removeFromFavorites(pattern);
-            favoritePatterns.delete(pattern);
-            heartIcon.textContent = 'favorite_border';
-            heartIcon.className = 'material-icons text-lg text-gray-400 hover:text-red-500 transition-colors';
-            // Make heart only visible on hover when not favorited
-            heartIcon.parentElement.className = heartIcon.parentElement.className.replace('opacity-100', 'opacity-0 group-hover:opacity-100');
-            showStatusMessage('Removed from favorites', 'success');
-        } else {
-            // Add to favorites
-            await addToFavorites(pattern);
-            favoritePatterns.add(pattern);
-            heartIcon.textContent = 'favorite';
-            heartIcon.className = 'material-icons text-lg text-red-500 hover:text-red-600 transition-colors';
-            // Make heart permanently visible when favorited
-            heartIcon.parentElement.className = heartIcon.parentElement.className.replace('opacity-0 group-hover:opacity-100', 'opacity-100');
-            showStatusMessage('Added to favorites', 'success');
-        }
-    } catch (error) {
-        console.error('Error toggling favorite:', error);
-        showStatusMessage('Failed to update favorites', 'error');
-    }
-}
-
-// Add pattern to favorites playlist
-async function addToFavorites(pattern) {
-    try {
-        // First, check if Favorites playlist exists
-        const checkResponse = await fetch('/get_playlist?name=Favorites');
-        
-        if (checkResponse.ok) {
-            // Playlist exists, add to it
-            const response = await fetch('/add_to_playlist', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json',
-                },
-                body: JSON.stringify({
-                    playlist_name: 'Favorites',
-                    pattern: pattern
-                })
-            });
-            
-            if (!response.ok) {
-                throw new Error('Failed to add to favorites playlist');
-            }
-        } else {
-            // Playlist doesn't exist, create it with this pattern
-            const response = await fetch('/create_playlist', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json',
-                },
-                body: JSON.stringify({
-                    playlist_name: 'Favorites',
-                    files: [pattern]
-                })
-            });
-            
-            if (!response.ok) {
-                throw new Error('Failed to create favorites playlist');
-            }
-        }
-    } catch (error) {
-        throw new Error(`Failed to add to favorites: ${error.message}`);
-    }
-}
-
-// Remove pattern from favorites playlist
-async function removeFromFavorites(pattern) {
-    try {
-        // Get current favorites playlist
-        const getResponse = await fetch('/get_playlist?name=Favorites');
-        if (!getResponse.ok) return; // No favorites playlist
-        
-        const currentFavorites = await getResponse.json();
-        const updatedFavorites = currentFavorites.files.filter(p => p !== pattern);
-        
-        // Update the playlist
-        const updateResponse = await fetch('/modify_playlist', {
-            method: 'POST',
-            headers: {
-                'Content-Type': 'application/json',
-            },
-            body: JSON.stringify({
-                playlist_name: 'Favorites',
-                files: updatedFavorites
-            })
-        });
-        
-        if (!updateResponse.ok) {
-            throw new Error('Failed to update favorites playlist');
-        }
-    } catch (error) {
-        throw new Error(`Failed to remove from favorites: ${error.message}`);
-    }
-}
-
-// Update all heart icons based on current favorites
-function updateAllHeartIcons() {
-    favoritePatterns.forEach(pattern => {
-        const heartIcon = document.getElementById('heart-' + pattern.replace(/[^a-zA-Z0-9]/g, '_'));
-        if (heartIcon) {
-            heartIcon.textContent = 'favorite';
-            heartIcon.className = 'material-icons text-lg text-red-500 hover:text-red-600 transition-colors';
-            // Make heart permanently visible when favorited
-            heartIcon.parentElement.className = heartIcon.parentElement.className.replace('opacity-0 group-hover:opacity-100', 'opacity-100');
-        }
-    });
-} 

+ 0 - 828
static/js/led-control.js

@@ -1,828 +0,0 @@
-// LED Control Page - Unified interface for WLED and DW LEDs
-
-let ledConfig = null;
-
-// Utility function to show status messages
-function showStatus(message, type = 'info') {
-    const statusDiv = document.getElementById('dw-leds-status');
-    if (!statusDiv) return;
-
-    const iconMap = {
-        'success': 'check_circle',
-        'error': 'error',
-        'warning': 'warning',
-        'info': 'info'
-    };
-
-    const colorMap = {
-        'success': 'text-green-700 bg-green-50 border-green-200',
-        'error': 'text-red-700 bg-red-50 border-red-200',
-        'warning': 'text-amber-700 bg-amber-50 border-amber-200',
-        'info': 'text-gray-700 bg-gray-100 border-slate-200'
-    };
-
-    const icon = iconMap[type] || 'info';
-    const colorClass = colorMap[type] || colorMap.info;
-
-    statusDiv.className = `p-4 rounded-lg border ${colorClass}`;
-    statusDiv.innerHTML = `
-        <div class="flex items-center gap-2">
-            <span class="material-icons">${icon}</span>
-            <span class="text-sm">${message}</span>
-        </div>
-    `;
-}
-
-// Initialize the page based on LED configuration
-async function initializeLedPage() {
-    try {
-        const response = await fetch('/get_led_config');
-        if (!response.ok) throw new Error('Failed to fetch LED config');
-
-        ledConfig = await response.json();
-
-        const notConfigured = document.getElementById('led-not-configured');
-        const wledContainer = document.getElementById('wled-container');
-        const dwLedsContainer = document.getElementById('dw-leds-container');
-
-        // Hide all containers first
-        notConfigured.classList.add('hidden');
-        wledContainer.classList.add('hidden');
-        dwLedsContainer.classList.add('hidden');
-
-        if (ledConfig.provider === 'wled' && ledConfig.wled_ip) {
-            // Show WLED iframe
-            wledContainer.classList.remove('hidden');
-            const wledFrame = document.getElementById('wled-frame');
-            if (wledFrame) {
-                wledFrame.src = `http://${ledConfig.wled_ip}`;
-            }
-        } else if (ledConfig.provider === 'dw_leds') {
-            // Show DW LEDs controls
-            dwLedsContainer.classList.remove('hidden');
-            await initializeDWLedsControls();
-        } else {
-            // Show not configured message
-            notConfigured.classList.remove('hidden');
-        }
-    } catch (error) {
-        console.error('Error initializing LED page:', error);
-        document.getElementById('led-not-configured').classList.remove('hidden');
-    }
-}
-
-// Initialize DW LEDs controls
-async function initializeDWLedsControls() {
-    // Check status and load available effects/palettes
-    await checkDWLedsStatus();
-    await loadEffectsAndPalettes();
-
-    // Power toggle button
-    document.getElementById('dw-leds-power-toggle')?.addEventListener('click', async () => {
-        try {
-            const response = await fetch('/api/dw_leds/power', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ state: 2 })  // Toggle
-            });
-
-            if (!response.ok) throw new Error(`HTTP ${response.status}`);
-            const data = await response.json();
-
-            if (data.connected) {
-                showStatus(`Power ${data.power_on ? 'ON' : 'OFF'}`, 'success');
-                await checkDWLedsStatus();
-            } else {
-                showStatus(data.error || 'Failed to toggle power', 'error');
-            }
-        } catch (error) {
-            showStatus(`Failed to toggle power: ${error.message}`, 'error');
-        }
-    });
-
-    // Brightness slider
-    const brightnessSlider = document.getElementById('dw-leds-brightness');
-    const brightnessValue = document.getElementById('dw-leds-brightness-value');
-
-    brightnessSlider?.addEventListener('input', (e) => {
-        brightnessValue.textContent = `${e.target.value}%`;
-    });
-
-    brightnessSlider?.addEventListener('change', async (e) => {
-        try {
-            const response = await fetch('/api/dw_leds/brightness', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ value: parseInt(e.target.value) })
-            });
-
-            if (!response.ok) throw new Error(`HTTP ${response.status}`);
-            const data = await response.json();
-
-            if (data.connected) {
-                showStatus(`Brightness set to ${e.target.value}%`, 'success');
-            } else {
-                showStatus(data.error || 'Failed to set brightness', 'error');
-            }
-        } catch (error) {
-            showStatus(`Failed to set brightness: ${error.message}`, 'error');
-        }
-    });
-
-    // Effect color pickers - apply immediately on change
-    document.querySelectorAll('.effect-color-picker').forEach(picker => {
-        picker.addEventListener('change', async () => {
-            const color1 = document.getElementById('dw-leds-color1')?.value;
-            const color2 = document.getElementById('dw-leds-color2')?.value;
-            const color3 = document.getElementById('dw-leds-color3')?.value;
-
-            if (color1 && color2 && color3) {
-                await applyAllColors(color1, color2, color3);
-            }
-        });
-    });
-
-    // Effect selector
-    document.getElementById('dw-leds-effect-select')?.addEventListener('change', async (e) => {
-        const effectId = parseInt(e.target.value);
-        if (isNaN(effectId)) return;
-
-        try {
-            const response = await fetch('/api/dw_leds/effect', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ effect_id: effectId })
-            });
-
-            if (!response.ok) throw new Error(`HTTP ${response.status}`);
-            const data = await response.json();
-
-            if (data.connected) {
-                showStatus(`Effect changed`, 'success');
-                // Update power button state if backend auto-powered on
-                if (data.power_on !== undefined) {
-                    updatePowerButtonUI(data.power_on);
-                }
-            } else {
-                showStatus(data.error || 'Failed to set effect', 'error');
-            }
-        } catch (error) {
-            showStatus(`Failed to set effect: ${error.message}`, 'error');
-        }
-    });
-
-    // Palette selector
-    document.getElementById('dw-leds-palette-select')?.addEventListener('change', async (e) => {
-        const paletteId = parseInt(e.target.value);
-        if (isNaN(paletteId)) return;
-
-        try {
-            const response = await fetch('/api/dw_leds/palette', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ palette_id: paletteId })
-            });
-
-            if (!response.ok) throw new Error(`HTTP ${response.status}`);
-            const data = await response.json();
-
-            if (data.connected) {
-                showStatus(`Palette changed`, 'success');
-                // Update power button state if backend auto-powered on
-                if (data.power_on !== undefined) {
-                    updatePowerButtonUI(data.power_on);
-                }
-            } else {
-                showStatus(data.error || 'Failed to set palette', 'error');
-            }
-        } catch (error) {
-            showStatus(`Failed to set palette: ${error.message}`, 'error');
-        }
-    });
-
-    // Speed slider
-    const speedSlider = document.getElementById('dw-leds-speed');
-    const speedValue = document.getElementById('dw-leds-speed-value');
-
-    speedSlider?.addEventListener('input', (e) => {
-        speedValue.textContent = e.target.value;
-    });
-
-    speedSlider?.addEventListener('change', async (e) => {
-        try {
-            const response = await fetch('/api/dw_leds/speed', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ speed: parseInt(e.target.value) })
-            });
-
-            if (!response.ok) throw new Error(`HTTP ${response.status}`);
-            const data = await response.json();
-
-            if (data.connected) {
-                showStatus(`Speed updated`, 'success');
-            } else {
-                showStatus(data.error || 'Failed to set speed', 'error');
-            }
-        } catch (error) {
-            showStatus(`Failed to set speed: ${error.message}`, 'error');
-        }
-    });
-
-    // Intensity slider
-    const intensitySlider = document.getElementById('dw-leds-intensity');
-    const intensityValue = document.getElementById('dw-leds-intensity-value');
-
-    intensitySlider?.addEventListener('input', (e) => {
-        intensityValue.textContent = e.target.value;
-    });
-
-    intensitySlider?.addEventListener('change', async (e) => {
-        try {
-            const response = await fetch('/api/dw_leds/intensity', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({ intensity: parseInt(e.target.value) })
-            });
-
-            if (!response.ok) throw new Error(`HTTP ${response.status}`);
-            const data = await response.json();
-
-            if (data.connected) {
-                showStatus(`Intensity updated`, 'success');
-            } else {
-                showStatus(data.error || 'Failed to set intensity', 'error');
-            }
-        } catch (error) {
-            showStatus(`Failed to set intensity: ${error.message}`, 'error');
-        }
-    });
-
-    // Save Current Idle Effect
-    document.getElementById('dw-leds-save-current-idle')?.addEventListener('click', async () => {
-        await saveCurrentEffectSettings('idle');
-    });
-
-    // Clear Idle Effect
-    document.getElementById('dw-leds-clear-idle')?.addEventListener('click', async () => {
-        await clearEffectSettings('idle');
-    });
-
-    // Save Current Playing Effect
-    document.getElementById('dw-leds-save-current-playing')?.addEventListener('click', async () => {
-        await saveCurrentEffectSettings('playing');
-    });
-
-    // Clear Playing Effect
-    document.getElementById('dw-leds-clear-playing')?.addEventListener('click', async () => {
-        await clearEffectSettings('playing');
-    });
-
-    // Load and display saved effect settings
-    await loadEffectSettings();
-
-    // Idle timeout controls
-    await loadIdleTimeout();
-
-    const idleTimeoutEnabled = document.getElementById('dw-leds-idle-timeout-enabled');
-    const idleTimeoutSettings = document.getElementById('idle-timeout-settings');
-    const idleTimeoutDisabledHelp = document.getElementById('idle-timeout-disabled-help');
-
-    // Toggle idle timeout settings visibility and help text
-    idleTimeoutEnabled?.addEventListener('change', (e) => {
-        const isEnabled = e.target.checked;
-
-        if (isEnabled) {
-            idleTimeoutSettings?.classList.remove('opacity-50', 'pointer-events-none');
-            idleTimeoutDisabledHelp?.classList.add('hidden');
-        } else {
-            idleTimeoutSettings?.classList.add('opacity-50', 'pointer-events-none');
-            idleTimeoutDisabledHelp?.classList.remove('hidden');
-        }
-
-        // Auto-save when toggle changes for better UX
-        saveIdleTimeout();
-    });
-
-    // Save idle timeout settings
-    document.getElementById('dw-leds-save-idle-timeout')?.addEventListener('click', async () => {
-        await saveIdleTimeout();
-    });
-
-    // Update remaining time periodically
-    let idleTimeoutInterval = setInterval(updateIdleTimeoutRemaining, 60000); // Update every minute
-
-    // Clean up interval when page unloads
-    window.addEventListener('beforeunload', () => {
-        if (idleTimeoutInterval) {
-            clearInterval(idleTimeoutInterval);
-            idleTimeoutInterval = null;
-        }
-    });
-
-    // Initialize Coloris color picker for effect colors
-    initializeColoris();
-}
-
-// Save current LED settings as idle or playing effect
-async function saveCurrentEffectSettings(type) {
-    try {
-        const effectId = parseInt(document.getElementById('dw-leds-effect-select')?.value) || 0;
-        const paletteId = parseInt(document.getElementById('dw-leds-palette-select')?.value) || 0;
-        const speed = parseInt(document.getElementById('dw-leds-speed')?.value) || 128;
-        const intensity = parseInt(document.getElementById('dw-leds-intensity')?.value) || 128;
-
-        // Get effect colors
-        const color1 = document.getElementById('dw-leds-color1')?.value || '#ff0000';
-        const color2 = document.getElementById('dw-leds-color2')?.value || '#000000';
-        const color3 = document.getElementById('dw-leds-color3')?.value || '#0000ff';
-
-        const settings = {
-            type: type,  // 'idle' or 'playing'
-            effect_id: effectId,
-            palette_id: paletteId,
-            speed: speed,
-            intensity: intensity,
-            color1: color1,
-            color2: color2,
-            color3: color3
-        };
-
-        const response = await fetch('/api/dw_leds/save_effect_settings', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify(settings)
-        });
-
-        if (!response.ok) throw new Error(`HTTP ${response.status}`);
-
-        await response.json();
-        showStatus(`${type.charAt(0).toUpperCase() + type.slice(1)} effect settings saved successfully`, 'success');
-
-        // Refresh display
-        await loadEffectSettings();
-    } catch (error) {
-        showStatus(`Failed to save ${type} effect settings: ${error.message}`, 'error');
-    }
-}
-
-// Clear effect settings
-async function clearEffectSettings(type) {
-    try {
-        const response = await fetch('/api/dw_leds/clear_effect_settings', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ type: type })
-        });
-
-        if (!response.ok) throw new Error(`HTTP ${response.status}`);
-
-        showStatus(`${type.charAt(0).toUpperCase() + type.slice(1)} effect cleared`, 'success');
-
-        // Refresh display
-        await loadEffectSettings();
-    } catch (error) {
-        showStatus(`Failed to clear ${type} effect: ${error.message}`, 'error');
-    }
-}
-
-// Load and display saved effect settings
-async function loadEffectSettings() {
-    try {
-        const response = await fetch('/api/dw_leds/get_effect_settings');
-        if (!response.ok) return;
-
-        const data = await response.json();
-
-        // Display idle settings
-        const idleDisplay = document.getElementById('idle-settings-display');
-        if (idleDisplay) {
-            idleDisplay.textContent = formatEffectSettings(data.idle_effect);
-        }
-
-        // Display playing settings
-        const playingDisplay = document.getElementById('playing-settings-display');
-        if (playingDisplay) {
-            playingDisplay.textContent = formatEffectSettings(data.playing_effect);
-        }
-    } catch (error) {
-        console.error('Failed to load effect settings:', error);
-    }
-}
-
-// Format effect settings for display
-function formatEffectSettings(settings) {
-    if (!settings) {
-        return 'Not configured (do nothing)';
-    }
-
-    const parts = [];
-
-    // Get effect name from select (if available)
-    const effectSelect = document.getElementById('dw-leds-effect-select');
-    if (effectSelect && settings.effect_id !== undefined) {
-        const effectOption = effectSelect.querySelector(`option[value="${settings.effect_id}"]`);
-        parts.push(`Effect: ${effectOption ? effectOption.textContent : settings.effect_id}`);
-    }
-
-    // Get palette name from select (if available)
-    const paletteSelect = document.getElementById('dw-leds-palette-select');
-    if (paletteSelect && settings.palette_id !== undefined) {
-        const paletteOption = paletteSelect.querySelector(`option[value="${settings.palette_id}"]`);
-        parts.push(`Palette: ${paletteOption ? paletteOption.textContent : settings.palette_id}`);
-    }
-
-    if (settings.speed !== undefined) {
-        parts.push(`Speed: ${settings.speed}`);
-    }
-
-    if (settings.intensity !== undefined) {
-        parts.push(`Intensity: ${settings.intensity}`);
-    }
-
-    if (settings.color1) {
-        parts.push(`Colors: ${settings.color1}, ${settings.color2 || '#000000'}, ${settings.color3 || '#0000ff'}`);
-    }
-
-    return parts.join(' | ');
-}
-
-// Load idle timeout settings
-async function loadIdleTimeout() {
-    try {
-        const response = await fetch('/api/dw_leds/idle_timeout');
-        if (!response.ok) return;
-
-        const data = await response.json();
-
-        const enabledCheckbox = document.getElementById('dw-leds-idle-timeout-enabled');
-        const minutesInput = document.getElementById('dw-leds-idle-timeout-minutes');
-        const idleTimeoutSettings = document.getElementById('idle-timeout-settings');
-        const idleTimeoutDisabledHelp = document.getElementById('idle-timeout-disabled-help');
-
-        if (enabledCheckbox) {
-            enabledCheckbox.checked = data.enabled;
-        }
-
-        if (minutesInput) {
-            minutesInput.value = data.minutes;
-        }
-
-        // Set initial state of settings panel and help text
-        if (data.enabled) {
-            idleTimeoutSettings?.classList.remove('opacity-50', 'pointer-events-none');
-            idleTimeoutDisabledHelp?.classList.add('hidden');
-        } else {
-            idleTimeoutSettings?.classList.add('opacity-50', 'pointer-events-none');
-            idleTimeoutDisabledHelp?.classList.remove('hidden');
-        }
-
-        // Update remaining time display
-        updateIdleTimeoutRemainingDisplay(data.remaining_minutes);
-    } catch (error) {
-        console.error('Failed to load idle timeout settings:', error);
-    }
-}
-
-// Save idle timeout settings
-async function saveIdleTimeout() {
-    try {
-        const enabled = document.getElementById('dw-leds-idle-timeout-enabled')?.checked || false;
-        const minutes = parseInt(document.getElementById('dw-leds-idle-timeout-minutes')?.value) || 30;
-
-        const response = await fetch('/api/dw_leds/idle_timeout', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ enabled, minutes })
-        });
-
-        if (!response.ok) throw new Error(`HTTP ${response.status}`);
-        const data = await response.json();
-
-        if (data.success) {
-            showStatus(`Idle timeout ${enabled ? 'enabled' : 'disabled'} (${minutes} minutes)`, 'success');
-            await loadIdleTimeout(); // Reload to get updated remaining time
-        } else {
-            showStatus('Failed to save idle timeout settings', 'error');
-        }
-    } catch (error) {
-        showStatus(`Failed to save idle timeout: ${error.message}`, 'error');
-    }
-}
-
-// Update idle timeout remaining time
-async function updateIdleTimeoutRemaining() {
-    try {
-        const response = await fetch('/api/dw_leds/idle_timeout');
-        if (!response.ok) return;
-
-        const data = await response.json();
-        updateIdleTimeoutRemainingDisplay(data.remaining_minutes);
-    } catch (error) {
-        console.error('Failed to update idle timeout remaining:', error);
-    }
-}
-
-// Update idle timeout remaining time display
-function updateIdleTimeoutRemainingDisplay(remainingMinutes) {
-    const remainingDiv = document.getElementById('idle-timeout-remaining');
-    const remainingDisplay = document.getElementById('idle-timeout-remaining-display');
-
-    if (!remainingDiv || !remainingDisplay) return;
-
-    if (remainingMinutes !== null && remainingMinutes !== undefined) {
-        remainingDiv.classList.remove('hidden');
-        if (remainingMinutes <= 0) {
-            remainingDisplay.textContent = 'Timeout expired - LEDs will turn off';
-        } else if (remainingMinutes < 1) {
-            remainingDisplay.textContent = 'Less than 1 minute';
-        } else {
-            const hours = Math.floor(remainingMinutes / 60);
-            const mins = Math.round(remainingMinutes % 60);
-            if (hours > 0) {
-                remainingDisplay.textContent = `${hours}h ${mins}m`;
-            } else {
-                remainingDisplay.textContent = `${mins} minutes`;
-            }
-        }
-    } else {
-        remainingDiv.classList.add('hidden');
-    }
-}
-
-// Helper function to apply all effect colors
-async function applyAllColors(hexColor1, hexColor2, hexColor3) {
-    try {
-        const payload = {};
-
-        if (hexColor1) {
-            const r = parseInt(hexColor1.slice(1, 3), 16);
-            const g = parseInt(hexColor1.slice(3, 5), 16);
-            const b = parseInt(hexColor1.slice(5, 7), 16);
-            payload.color1 = [r, g, b];
-        }
-
-        if (hexColor2) {
-            const r = parseInt(hexColor2.slice(1, 3), 16);
-            const g = parseInt(hexColor2.slice(3, 5), 16);
-            const b = parseInt(hexColor2.slice(5, 7), 16);
-            payload.color2 = [r, g, b];
-        }
-
-        if (hexColor3) {
-            const r = parseInt(hexColor3.slice(1, 3), 16);
-            const g = parseInt(hexColor3.slice(3, 5), 16);
-            const b = parseInt(hexColor3.slice(5, 7), 16);
-            payload.color3 = [r, g, b];
-        }
-
-        const response = await fetch('/api/dw_leds/colors', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify(payload)
-        });
-
-        if (!response.ok) throw new Error(`HTTP ${response.status}`);
-        const data = await response.json();
-
-        if (data.connected) {
-            showStatus(`Effect colors updated`, 'success');
-        } else {
-            showStatus(data.error || 'Failed to set colors', 'error');
-        }
-    } catch (error) {
-        showStatus(`Failed to set colors: ${error.message}`, 'error');
-    }
-}
-
-// Load available effects and palettes
-async function loadEffectsAndPalettes() {
-    try {
-        // Load effects
-        const effectsResponse = await fetch('/api/dw_leds/effects');
-        if (effectsResponse.ok) {
-            const effectsData = await effectsResponse.json();
-            const effectSelect = document.getElementById('dw-leds-effect-select');
-            const idleEffectSelect = document.getElementById('dw-leds-idle-effect');
-            const playingEffectSelect = document.getElementById('dw-leds-playing-effect');
-
-            if (effectSelect && effectsData.effects) {
-                effectSelect.innerHTML = '';
-                // Sort effects alphabetically by name
-                const sortedEffects = [...effectsData.effects].sort((a, b) =>
-                    a[1].localeCompare(b[1])
-                );
-                sortedEffects.forEach(([id, name]) => {
-                    const option = document.createElement('option');
-                    option.value = id;
-                    option.textContent = name;
-                    effectSelect.appendChild(option);
-                });
-            }
-
-            // Add effects to automation selectors
-            if (idleEffectSelect && effectsData.effects) {
-                idleEffectSelect.innerHTML = '<option value="off">Off</option>';
-                // Sort effects alphabetically by name
-                const sortedEffects = [...effectsData.effects].sort((a, b) =>
-                    a[1].localeCompare(b[1])
-                );
-                sortedEffects.forEach(([, name]) => {
-                    const option = document.createElement('option');
-                    option.value = name.toLowerCase();
-                    option.textContent = name;
-                    idleEffectSelect.appendChild(option);
-                });
-            }
-
-            if (playingEffectSelect && effectsData.effects) {
-                playingEffectSelect.innerHTML = '<option value="off">Off</option>';
-                // Sort effects alphabetically by name
-                const sortedEffects = [...effectsData.effects].sort((a, b) =>
-                    a[1].localeCompare(b[1])
-                );
-                sortedEffects.forEach(([, name]) => {
-                    const option = document.createElement('option');
-                    option.value = name.toLowerCase();
-                    option.textContent = name;
-                    playingEffectSelect.appendChild(option);
-                });
-            }
-
-            // Load saved automation settings
-            const configResponse = await fetch('/get_led_config');
-            if (configResponse.ok) {
-                const config = await configResponse.json();
-                if (idleEffectSelect && config.dw_led_idle_effect) {
-                    idleEffectSelect.value = config.dw_led_idle_effect;
-                }
-                if (playingEffectSelect && config.dw_led_playing_effect) {
-                    playingEffectSelect.value = config.dw_led_playing_effect;
-                }
-            }
-        }
-
-        // Load palettes
-        const palettesResponse = await fetch('/api/dw_leds/palettes');
-        if (palettesResponse.ok) {
-            const palettesData = await palettesResponse.json();
-            const paletteSelect = document.getElementById('dw-leds-palette-select');
-
-            if (paletteSelect && palettesData.palettes) {
-                paletteSelect.innerHTML = '';
-                // Sort palettes alphabetically by name
-                const sortedPalettes = [...palettesData.palettes].sort((a, b) =>
-                    a[1].localeCompare(b[1])
-                );
-                sortedPalettes.forEach(([id, name]) => {
-                    const option = document.createElement('option');
-                    option.value = id;
-                    option.textContent = name;
-                    paletteSelect.appendChild(option);
-                });
-            }
-        }
-    } catch (error) {
-        console.error('Failed to load effects and palettes:', error);
-        showStatus('Failed to load effects and palettes', 'error');
-    }
-}
-
-// Helper function to update power button UI based on power state
-function updatePowerButtonUI(powerOn) {
-    const powerButton = document.getElementById('dw-leds-power-toggle');
-    const powerButtonText = document.getElementById('dw-leds-power-text');
-
-    if (powerButton && powerButtonText) {
-        if (powerOn) {
-            powerButton.className = 'flex items-center justify-center gap-2 rounded-lg bg-red-600 px-4 py-3 text-sm font-semibold text-white shadow-md hover:bg-red-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-offset-2';
-            powerButtonText.textContent = 'Turn OFF';
-        } else {
-            powerButton.className = 'flex items-center justify-center gap-2 rounded-lg bg-green-600 px-4 py-3 text-sm font-semibold text-white shadow-md hover:bg-green-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-offset-2';
-            powerButtonText.textContent = 'Turn ON';
-        }
-    }
-}
-
-// Check DW LEDs connection status
-async function checkDWLedsStatus() {
-    try {
-        const response = await fetch('/api/dw_leds/status');
-        if (!response.ok) throw new Error(`HTTP ${response.status}`);
-
-        const data = await response.json();
-
-        if (data.connected) {
-            const powerState = data.power_on ? 'ON' : 'OFF';
-            showStatus(`Connected: ${data.num_leds} LEDs on GPIO ${data.gpio_pin} - Power: ${powerState}`, 'success');
-
-            // Update power button appearance
-            updatePowerButtonUI(data.power_on);
-
-            // Update slider values
-            const brightnessSlider = document.getElementById('dw-leds-brightness');
-            const brightnessValue = document.getElementById('dw-leds-brightness-value');
-            if (brightnessSlider && data.brightness !== undefined) {
-                brightnessSlider.value = data.brightness;
-                if (brightnessValue) brightnessValue.textContent = `${data.brightness}%`;
-            }
-
-            const speedSlider = document.getElementById('dw-leds-speed');
-            const speedValue = document.getElementById('dw-leds-speed-value');
-            if (speedSlider && data.speed !== undefined) {
-                speedSlider.value = data.speed;
-                if (speedValue) speedValue.textContent = data.speed;
-            }
-
-            const intensitySlider = document.getElementById('dw-leds-intensity');
-            const intensityValue = document.getElementById('dw-leds-intensity-value');
-            if (intensitySlider && data.intensity !== undefined) {
-                intensitySlider.value = data.intensity;
-                if (intensityValue) intensityValue.textContent = data.intensity;
-            }
-
-            // Update effect and palette selectors
-            const effectSelect = document.getElementById('dw-leds-effect-select');
-            if (effectSelect && data.current_effect !== undefined) {
-                effectSelect.value = data.current_effect;
-            }
-
-            const paletteSelect = document.getElementById('dw-leds-palette-select');
-            if (paletteSelect && data.current_palette !== undefined) {
-                paletteSelect.value = data.current_palette;
-            }
-
-            // Update color pickers if colors are provided
-            if (data.colors && Array.isArray(data.colors)) {
-                const color1 = document.getElementById('dw-leds-color1');
-                const color2 = document.getElementById('dw-leds-color2');
-                const color3 = document.getElementById('dw-leds-color3');
-
-                if (color1 && data.colors[0]) {
-                    color1.value = data.colors[0];
-                    updateColorPickerStyle(color1, data.colors[0]);
-                }
-                if (color2 && data.colors[1]) {
-                    color2.value = data.colors[1];
-                    updateColorPickerStyle(color2, data.colors[1]);
-                }
-                if (color3 && data.colors[2]) {
-                    color3.value = data.colors[2];
-                    updateColorPickerStyle(color3, data.colors[2]);
-                }
-            }
-        } else {
-            // Show error message from controller
-            const errorMsg = data.error || 'Connection failed';
-            showStatus(errorMsg, 'error');
-        }
-    } catch (error) {
-        showStatus(`Cannot connect to DW LEDs: ${error.message}`, 'error');
-    }
-}
-
-// Helper function to update color picker background
-function updateColorPickerStyle(input, color) {
-    if (!input || !color) return;
-    input.style.backgroundColor = color;
-}
-
-// Initialize Coloris color picker
-function initializeColoris() {
-    // Initialize Coloris with custom configuration
-    Coloris({
-        theme: 'polaroid',
-        themeMode: 'auto',
-        formatToggle: true,
-        alpha: false,  // No transparency for LED colors
-        swatches: [
-            '#ff0000',  // Red
-            '#00ff00',  // Green
-            '#0000ff',  // Blue
-            '#ffff00',  // Yellow
-            '#ff00ff',  // Magenta
-            '#00ffff',  // Cyan
-            '#ff8000',  // Orange
-            '#ffffff',  // White
-            '#2a9d8f',  // Teal
-            '#e9c46a',  // Sand
-            'coral',    // Coral
-            'Crimson'   // Crimson
-        ],
-        onChange: (color, input) => {
-            // Update the input background to show the selected color
-            updateColorPickerStyle(input, color);
-        }
-    });
-
-    // Apply Coloris to all effect color pickers and set initial background colors
-    const colorPickers = document.querySelectorAll('.effect-color-picker');
-    colorPickers.forEach(picker => {
-        picker.setAttribute('data-coloris', '');
-        // Set initial background color and text color
-        updateColorPickerStyle(picker, picker.value);
-    });
-}
-
-// Initialize on page load
-document.addEventListener('DOMContentLoaded', initializeLedPage);

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 29
static/js/opencv.js


+ 0 - 1935
static/js/playlists.js

@@ -1,1935 +0,0 @@
-// Constants for log message types
-const LOG_TYPE = {
-    SUCCESS: 'success',
-    WARNING: 'warning',
-    ERROR: 'error',
-    INFO: 'info',
-    DEBUG: 'debug'
-};
-
-// Global variables
-let allPlaylists = [];
-let currentPlaylist = null;
-let availablePatterns = [];
-let availablePatternsWithMetadata = []; // Enhanced pattern data with metadata
-let filteredPatterns = [];
-let selectedPatterns = new Set();
-let previewCache = new Map();
-let intersectionObserver = null;
-let searchTimeout = null;
-
-// Sorting and filtering state
-let currentSort = { field: 'name', direction: 'asc' };
-let currentFilters = { category: 'all' };
-
-// Mobile navigation state
-let isMobileView = false;
-
-// Global variables for batching lazy loading
-let pendingPatterns = new Map(); // pattern -> element mapping
-let batchTimeout = null;
-const BATCH_SIZE = 40; // Increased batch size for better performance
-const BATCH_DELAY = 150; // Wait 150ms to collect more patterns before batching
-
-// Shared caching for patterns list (persistent across sessions)
-const PATTERNS_CACHE_KEY = 'dune_weaver_patterns_cache';
-
-// IndexedDB cache for preview images with size management
-const PREVIEW_CACHE_DB_NAME = 'dune_weaver_previews';
-const PREVIEW_CACHE_DB_VERSION = 1;
-const PREVIEW_CACHE_STORE_NAME = 'previews';
-const MAX_CACHE_SIZE_MB = 200;
-const MAX_CACHE_SIZE_BYTES = MAX_CACHE_SIZE_MB * 1024 * 1024;
-
-let previewCacheDB = null;
-
-// --- Playback Settings Persistence ---
-const PLAYBACK_SETTINGS_KEY = 'dune_weaver_playback_settings';
-
-function savePlaybackSettings() {
-    const runMode = document.querySelector('input[name="run_playlist"]:checked')?.value || 'single';
-    const shuffle = document.getElementById('shuffleCheckbox')?.checked || false;
-    const pauseTime = document.getElementById('pauseTimeInput')?.value || '5';
-    const clearPattern = document.getElementById('clearPatternSelect')?.value || 'none';
-    const settings = { runMode, shuffle, pauseTime, clearPattern };
-    try {
-        localStorage.setItem(PLAYBACK_SETTINGS_KEY, JSON.stringify(settings));
-    } catch (e) {}
-}
-
-function restorePlaybackSettings() {
-    try {
-        const settings = JSON.parse(localStorage.getItem(PLAYBACK_SETTINGS_KEY));
-        if (!settings) return;
-        // Run mode
-        if (settings.runMode) {
-            const radio = document.querySelector(`input[name="run_playlist"][value="${settings.runMode}"]`);
-            if (radio) radio.checked = true;
-        }
-        // Shuffle
-        if (typeof settings.shuffle === 'boolean') {
-            const shuffleBox = document.getElementById('shuffleCheckbox');
-            if (shuffleBox) shuffleBox.checked = settings.shuffle;
-        }
-        // Pause time
-        if (settings.pauseTime) {
-            const pauseInput = document.getElementById('pauseTimeInput');
-            if (pauseInput) pauseInput.value = settings.pauseTime;
-        }
-        // Clear pattern
-        if (settings.clearPattern) {
-            const clearSel = document.getElementById('clearPatternSelect');
-            if (clearSel) clearSel.value = settings.clearPattern;
-        }
-    } catch (e) {}
-}
-
-// Attach listeners to save settings on change
-function setupPlaybackSettingsPersistence() {
-    document.querySelectorAll('input[name="run_playlist"]').forEach(radio => {
-        radio.addEventListener('change', savePlaybackSettings);
-    });
-    const shuffleBox = document.getElementById('shuffleCheckbox');
-    if (shuffleBox) shuffleBox.addEventListener('change', savePlaybackSettings);
-    const pauseInput = document.getElementById('pauseTimeInput');
-    if (pauseInput) pauseInput.addEventListener('input', savePlaybackSettings);
-    const clearSel = document.getElementById('clearPatternSelect');
-    if (clearSel) clearSel.addEventListener('change', savePlaybackSettings);
-}
-
-// --- End Playback Settings Persistence ---
-
-// --- Playlist Selection Persistence ---
-const LAST_PLAYLIST_KEY = 'dune_weaver_last_playlist';
-
-function saveLastSelectedPlaylist(playlistName) {
-    try {
-        localStorage.setItem(LAST_PLAYLIST_KEY, playlistName);
-    } catch (e) {}
-}
-
-function getLastSelectedPlaylist() {
-    try {
-        return localStorage.getItem(LAST_PLAYLIST_KEY);
-    } catch (e) { return null; }
-}
-// --- End Playlist Selection Persistence ---
-
-// Initialize IndexedDB for preview caching
-async function initPreviewCacheDB() {
-    if (previewCacheDB) return previewCacheDB;
-    
-    return new Promise((resolve, reject) => {
-        const request = indexedDB.open(PREVIEW_CACHE_DB_NAME, PREVIEW_CACHE_DB_VERSION);
-        
-        request.onerror = () => {
-            logMessage('Failed to open preview cache database', LOG_TYPE.ERROR);
-            reject(request.error);
-        };
-        
-        request.onsuccess = () => {
-            previewCacheDB = request.result;
-            logMessage('Preview cache database opened successfully', LOG_TYPE.DEBUG);
-            resolve(previewCacheDB);
-        };
-        
-        request.onupgradeneeded = (event) => {
-            const db = event.target.result;
-            
-            // Create object store for preview cache
-            const store = db.createObjectStore(PREVIEW_CACHE_STORE_NAME, { keyPath: 'pattern' });
-            store.createIndex('lastAccessed', 'lastAccessed', { unique: false });
-            store.createIndex('size', 'size', { unique: false });
-            
-            logMessage('Preview cache database schema created', LOG_TYPE.DEBUG);
-        };
-    });
-}
-
-// Get preview from IndexedDB cache
-async function getPreviewFromCache(pattern) {
-    try {
-        if (!previewCacheDB) await initPreviewCacheDB();
-        
-        const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
-        const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-        
-        return new Promise((resolve, reject) => {
-            const request = store.get(pattern);
-            
-            request.onsuccess = () => {
-                const result = request.result;
-                if (result) {
-                    // Update last accessed time
-                    result.lastAccessed = Date.now();
-                    store.put(result);
-                    resolve(result.data);
-                } else {
-                    resolve(null);
-                }
-            };
-            
-            request.onerror = () => reject(request.error);
-        });
-    } catch (error) {
-        logMessage(`Error getting preview from cache: ${error.message}`, LOG_TYPE.WARNING);
-        return null;
-    }
-}
-
-// Save preview to IndexedDB cache with size management
-// Clear a specific pattern from IndexedDB cache
-async function clearPatternFromIndexedDB(pattern) {
-    try {
-        if (!previewCacheDB) await initPreviewCacheDB();
-        
-        const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
-        const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-        
-        await new Promise((resolve, reject) => {
-            const deleteRequest = store.delete(pattern);
-            deleteRequest.onsuccess = () => {
-                logMessage(`Cleared ${pattern} from IndexedDB cache`, LOG_TYPE.DEBUG);
-                resolve();
-            };
-            deleteRequest.onerror = () => {
-                logMessage(`Failed to clear ${pattern} from IndexedDB: ${deleteRequest.error}`, LOG_TYPE.WARNING);
-                reject(deleteRequest.error);
-            };
-        });
-    } catch (error) {
-        logMessage(`Error clearing pattern from IndexedDB: ${error.message}`, LOG_TYPE.WARNING);
-    }
-}
-
-async function savePreviewToCache(pattern, previewData) {
-    try {
-        if (!previewCacheDB) await initPreviewCacheDB();
-        
-        // Validate preview data before attempting to fetch
-        if (!previewData || !previewData.image_data) {
-            logMessage(`Invalid preview data for ${pattern}, skipping cache save`, LOG_TYPE.WARNING);
-            return;
-        }
-        
-        // Convert preview URL to blob for size calculation
-        const response = await fetch(previewData.image_data);
-        const blob = await response.blob();
-        const size = blob.size;
-        
-        // Check if we need to free up space
-        await managePreviewCacheSize(size);
-        
-        const cacheEntry = {
-            pattern: pattern,
-            data: previewData,
-            size: size,
-            lastAccessed: Date.now(),
-            created: Date.now()
-        };
-        
-        const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
-        const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-        
-        return new Promise((resolve, reject) => {
-            const request = store.put(cacheEntry);
-            
-            request.onsuccess = () => {
-                logMessage(`Preview cached for ${pattern} (${(size / 1024).toFixed(1)}KB)`, LOG_TYPE.DEBUG);
-                resolve();
-            };
-            
-            request.onerror = () => reject(request.error);
-        });
-        
-    } catch (error) {
-        logMessage(`Error saving preview to cache: ${error.message}`, LOG_TYPE.WARNING);
-    }
-}
-
-// Manage cache size by removing least recently used items
-async function managePreviewCacheSize(newItemSize) {
-    try {
-        const currentSize = await getPreviewCacheSize();
-        
-        if (currentSize + newItemSize <= MAX_CACHE_SIZE_BYTES) {
-            return; // No cleanup needed
-        }
-        
-        logMessage(`Cache size would exceed limit (${((currentSize + newItemSize) / 1024 / 1024).toFixed(1)}MB), cleaning up...`, LOG_TYPE.DEBUG);
-        
-        const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
-        const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-        const index = store.index('lastAccessed');
-        
-        // Get all entries sorted by last accessed (oldest first)
-        const entries = await new Promise((resolve, reject) => {
-            const request = index.getAll();
-            request.onsuccess = () => resolve(request.result);
-            request.onerror = () => reject(request.error);
-        });
-        
-        // Sort by last accessed time (oldest first)
-        entries.sort((a, b) => a.lastAccessed - b.lastAccessed);
-        
-        let freedSpace = 0;
-        const targetSpace = newItemSize + (MAX_CACHE_SIZE_BYTES * 0.1); // Free 10% extra buffer
-        
-        for (const entry of entries) {
-            if (freedSpace >= targetSpace) break;
-            
-            await new Promise((resolve, reject) => {
-                const deleteRequest = store.delete(entry.pattern);
-                deleteRequest.onsuccess = () => {
-                    freedSpace += entry.size;
-                    logMessage(`Evicted cached preview for ${entry.pattern} (${(entry.size / 1024).toFixed(1)}KB)`, LOG_TYPE.DEBUG);
-                    resolve();
-                };
-                deleteRequest.onerror = () => reject(deleteRequest.error);
-            });
-        }
-        
-        logMessage(`Freed ${(freedSpace / 1024 / 1024).toFixed(1)}MB from preview cache`, LOG_TYPE.DEBUG);
-        
-    } catch (error) {
-        logMessage(`Error managing cache size: ${error.message}`, LOG_TYPE.WARNING);
-    }
-}
-
-// Get current cache size
-async function getPreviewCacheSize() {
-    try {
-        if (!previewCacheDB) return 0;
-        
-        const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readonly');
-        const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-        
-        return new Promise((resolve, reject) => {
-            const request = store.getAll();
-            
-            request.onsuccess = () => {
-                const totalSize = request.result.reduce((sum, entry) => sum + (entry.size || 0), 0);
-                resolve(totalSize);
-            };
-            
-            request.onerror = () => reject(request.error);
-        });
-        
-    } catch (error) {
-        logMessage(`Error getting cache size: ${error.message}`, LOG_TYPE.WARNING);
-        return 0;
-    }
-}
-
-// Clear preview cache
-async function clearPreviewCache() {
-    try {
-        if (!previewCacheDB) return;
-        
-        const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readwrite');
-        const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-        
-        return new Promise((resolve, reject) => {
-            const request = store.clear();
-            
-            request.onsuccess = () => {
-                logMessage('Preview cache cleared', LOG_TYPE.DEBUG);
-                resolve();
-            };
-            
-            request.onerror = () => reject(request.error);
-        });
-        
-    } catch (error) {
-        logMessage(`Error clearing preview cache: ${error.message}`, LOG_TYPE.WARNING);
-    }
-}
-
-// Get cache statistics
-async function getPreviewCacheStats() {
-    try {
-        const size = await getPreviewCacheSize();
-        const transaction = previewCacheDB.transaction([PREVIEW_CACHE_STORE_NAME], 'readonly');
-        const store = transaction.objectStore(PREVIEW_CACHE_STORE_NAME);
-        
-        const count = await new Promise((resolve, reject) => {
-            const request = store.count();
-            request.onsuccess = () => resolve(request.result);
-            request.onerror = () => reject(request.error);
-        });
-        
-        return {
-            count,
-            size,
-            sizeMB: size / 1024 / 1024,
-            maxSizeMB: MAX_CACHE_SIZE_MB,
-            utilizationPercent: (size / MAX_CACHE_SIZE_BYTES) * 100
-        };
-        
-    } catch (error) {
-        logMessage(`Error getting cache stats: ${error.message}`, LOG_TYPE.WARNING);
-        return { count: 0, size: 0, sizeMB: 0, maxSizeMB: MAX_CACHE_SIZE_MB, utilizationPercent: 0 };
-    }
-}
-
-// Initialize Intersection Observer for lazy loading
-function initializeIntersectionObserver() {
-    intersectionObserver = new IntersectionObserver((entries) => {
-        // Get all visible elements
-        const visibleElements = entries.filter(entry => entry.isIntersecting);
-        if (visibleElements.length === 0) return;
-
-        // Collect all visible patterns
-        const visiblePatterns = new Map();
-        visibleElements.forEach(entry => {
-            const patternElement = entry.target;
-            const pattern = patternElement.dataset.pattern;
-            if (pattern && !previewCache.has(pattern)) {
-                visiblePatterns.set(pattern, patternElement);
-                intersectionObserver.unobserve(patternElement);
-            }
-        });
-
-        // If we have visible patterns that need loading, add them to the batch
-        if (visiblePatterns.size > 0) {
-            // Add to pending batch
-            for (const [pattern, element] of visiblePatterns) {
-                pendingPatterns.set(pattern, element);
-            }
-
-            // Clear existing timeout and set new one
-            if (batchTimeout) {
-                clearTimeout(batchTimeout);
-            }
-
-            batchTimeout = setTimeout(() => {
-                processPendingBatch();
-            }, BATCH_DELAY);
-        }
-    }, {
-        rootMargin: '0px 0px 600px 0px', // Large bottom margin to trigger early as element approaches from bottom
-        threshold: 0.1
-    });
-}
-
-// Function to get visible patterns that are still loading
-function getVisibleLoadingPatterns() {
-    const visibleLoadingPatterns = new Map();
-    
-    // Get all pattern elements that are currently visible
-    const patternElements = document.querySelectorAll('[data-pattern]');
-    
-    patternElements.forEach(element => {
-        const pattern = element.dataset.pattern;
-        if (pattern && !previewCache.has(pattern)) {
-            // Check if element is visible (intersecting with viewport)
-            const rect = element.getBoundingClientRect();
-            const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
-            
-            if (isVisible) {
-                visibleLoadingPatterns.set(pattern, element);
-            }
-        }
-    });
-    
-    return visibleLoadingPatterns;
-}
-
-// Modified processPendingBatch to keep polling for loading previews
-async function processPendingBatch() {
-    if (pendingPatterns.size === 0) return;
-    
-    // Create a copy of current pending patterns and clear the original
-    const currentBatch = new Map(pendingPatterns);
-    pendingPatterns.clear();
-    batchTimeout = null;
-    
-    const patternsToLoad = Array.from(currentBatch.keys());
-    
-    try {
-        logMessage(`Loading ${patternsToLoad.length} pattern previews`, LOG_TYPE.DEBUG);
-        
-        const response = await fetch('/preview_thr_batch', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ file_names: patternsToLoad })
-        });
-
-        if (response.ok) {
-            const results = await response.json();
-            
-            // Process all results
-            for (const [pattern, data] of Object.entries(results)) {
-                const element = currentBatch.get(pattern);
-                const previewContainer = element?.querySelector('.pattern-preview');
-                
-                if (data && !data.error && data.image_data) {
-                    // Cache both in memory and IndexedDB
-                    previewCache.set(pattern, data);
-                    await savePreviewToCache(pattern, data);
-                    
-                    if (previewContainer) {
-                        previewContainer.innerHTML = ''; // Remove loading indicator
-                        previewContainer.innerHTML = `<img src="${data.image_data}" alt="Pattern Preview" class="w-full h-full object-cover rounded-full" />`;
-                    }
-                } else {
-                    previewCache.set(pattern, { error: true });
-                }
-            }
-        } else {
-            throw new Error(`HTTP error! status: ${response.status}`);
-        }
-    } catch (error) {
-        logMessage(`Error loading pattern preview batch: ${error.message}`, LOG_TYPE.ERROR);
-        // Mark as error in cache
-        for (const pattern of patternsToLoad) {
-            previewCache.set(pattern, { error: true });
-        }
-    }
-
-    // After processing, check for any visible loading previews and request them
-    const stillLoading = getVisibleLoadingPatterns();
-    if (stillLoading.size > 0) {
-        // Add to pendingPatterns and immediately process
-        for (const [pattern, element] of stillLoading) {
-            pendingPatterns.set(pattern, element);
-        }
-        await processPendingBatch();
-    }
-}
-
-// Function to show status message
-function showStatusMessage(message, type = 'success') {
-    const statusContainer = document.getElementById('status-message-container');
-    const statusMessage = document.getElementById('status-message');
-    
-    if (!statusContainer || !statusMessage) return;
-    
-    // Set message and color based on type
-    statusMessage.textContent = message;
-    statusMessage.className = `text-base font-semibold opacity-0 transform -translate-y-2 transition-all duration-300 ease-in-out px-4 py-2 rounded-lg shadow-lg ${
-        type === 'success' ? 'bg-green-50 text-green-700 border border-green-200' :
-        type === 'error' ? 'bg-red-50 text-red-700 border border-red-200' :
-        type === 'warning' ? 'bg-yellow-50 text-yellow-700 border border-yellow-200' :
-        'bg-blue-50 text-blue-700 border border-blue-200'
-    }`;
-    
-    // Show message with animation
-    requestAnimationFrame(() => {
-        statusMessage.classList.remove('opacity-0', '-translate-y-2');
-        statusMessage.classList.add('opacity-100', 'translate-y-0');
-    });
-    
-    // Hide message after 5 seconds
-    setTimeout(() => {
-        statusMessage.classList.remove('opacity-100', 'translate-y-0');
-        statusMessage.classList.add('opacity-0', '-translate-y-2');
-    }, 5000);
-}
-
-// Function to log messages
-function logMessage(message, type = LOG_TYPE.DEBUG) {
-    console.log(`[${type}] ${message}`);
-}
-
-// Load all playlists
-async function loadPlaylists() {
-    try {
-        const response = await fetch('/list_all_playlists');
-        if (response.ok) {
-            allPlaylists = await response.json();
-            displayPlaylists();
-            // Auto-select last selected using data attribute
-            const last = getLastSelectedPlaylist();
-            if (last && allPlaylists.includes(last)) {
-                setTimeout(() => {
-                    const nav = document.getElementById('playlistsNav');
-                    const el = nav.querySelector(`a[data-playlist-name="${last}"]`);
-                    if (el) el.click();
-                }, 0);
-            }
-        } else {
-            throw new Error('Failed to load playlists');
-        }
-    } catch (error) {
-        logMessage(`Error loading playlists: ${error.message}`, LOG_TYPE.ERROR);
-        showStatusMessage('Failed to load playlists', 'error');
-    }
-}
-
-// Display playlists in sidebar
-function displayPlaylists() {
-    const playlistsNav = document.getElementById('playlistsNav');
-    playlistsNav.innerHTML = '';
-
-    if (allPlaylists.length === 0) {
-        playlistsNav.innerHTML = `
-            <div class="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400">
-                <span class="text-sm">No playlists found</span>
-            </div>
-        `;
-        return;
-    }
-
-    allPlaylists.forEach(playlist => {
-        const playlistItem = document.createElement('a');
-        playlistItem.className = 'flex items-center gap-3 px-3 py-2.5 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-gray-100 transition-colors duration-150 cursor-pointer';
-        playlistItem.dataset.playlistName = playlist; // Add data attribute for easy lookup
-        playlistItem.innerHTML = `
-            <span class="material-icons text-lg text-gray-500 dark:text-gray-400">queue_music</span>
-            <span class="text-sm font-medium flex-1 truncate">${playlist}</span>
-            <span class="material-icons text-lg text-gray-400 dark:text-gray-500">chevron_right</span>
-        `;
-
-        playlistItem.addEventListener('click', () => selectPlaylist(playlist, playlistItem));
-        playlistsNav.appendChild(playlistItem);
-    });
-}
-
-// Select a playlist
-async function selectPlaylist(playlistName, element) {
-    // Remove active state from all playlist items
-    document.querySelectorAll('#playlistsNav a').forEach(item => {
-        item.classList.remove('text-gray-900', 'dark:text-gray-100', 'bg-gray-100', 'dark:bg-gray-700', 'font-semibold');
-        item.classList.add('text-gray-700', 'dark:text-gray-300', 'font-medium');
-    });
-
-    // Add active state to selected item
-    element.classList.remove('text-gray-700', 'dark:text-gray-300', 'font-medium');
-    element.classList.add('text-gray-900', 'dark:text-gray-100', 'bg-gray-100', 'dark:bg-gray-700', 'font-semibold');
-
-    // Update current playlist
-    currentPlaylist = playlistName;
-    
-    // Update header with playlist name, rename and delete buttons
-    const header = document.getElementById('currentPlaylistTitle');
-    header.innerHTML = `
-        <h1 class="text-gray-900 dark:text-gray-100 text-2xl font-semibold leading-tight truncate">${playlistName}</h1>
-        <div class="flex items-center gap-1 flex-shrink-0">
-            <button id="renamePlaylistBtn" class="p-1 rounded-lg hover:bg-blue-100 dark:hover:bg-blue-900/20 text-gray-500 dark:text-gray-500 hover:text-blue-500 dark:hover:text-blue-400 transition-all duration-150" title="Rename playlist">
-                <span class="material-icons text-lg">edit</span>
-            </button>
-            <button id="deletePlaylistBtn" class="p-1 rounded-lg hover:bg-red-100 dark:hover:bg-red-900/20 text-gray-500 dark:text-gray-500 hover:text-red-500 dark:hover:text-red-400 transition-all duration-150" title="Delete playlist">
-                <span class="material-icons text-lg">delete</span>
-            </button>
-        </div>
-    `;
-
-    // Add button event listeners
-    document.getElementById('renamePlaylistBtn').addEventListener('click', () => openRenameModal(playlistName));
-    document.getElementById('deletePlaylistBtn').addEventListener('click', () => deletePlaylist(playlistName));
-
-    // Enable buttons
-    document.getElementById('addPatternsBtn').disabled = false;
-    document.getElementById('runPlaylistBtn').disabled = false;
-
-    // Save last selected
-    saveLastSelectedPlaylist(playlistName);
-
-    // Show playlist details on mobile
-    showPlaylistDetails();
-
-    // Load playlist patterns
-    await loadPlaylistPatterns(playlistName);
-}
-
-// Load patterns for selected playlist
-async function loadPlaylistPatterns(playlistName) {
-    try {
-        const response = await fetch(`/get_playlist?name=${encodeURIComponent(playlistName)}`);
-        if (response.ok) {
-            const playlistData = await response.json();
-            displayPlaylistPatterns(playlistData.files || []);
-            
-            // Show playback settings
-            document.getElementById('playbackSettings').classList.remove('hidden');
-        } else {
-            throw new Error('Failed to load playlist patterns');
-        }
-    } catch (error) {
-        logMessage(`Error loading playlist patterns: ${error.message}`, LOG_TYPE.ERROR);
-        showStatusMessage('Failed to load playlist patterns', 'error');
-    }
-}
-
-// Display patterns in the current playlist
-async function displayPlaylistPatterns(patterns) {
-    const patternsGrid = document.getElementById('patternsGrid');
-
-    if (patterns.length === 0) {
-        patternsGrid.innerHTML = `
-            <div class="flex items-center justify-center col-span-full py-12 text-gray-500 dark:text-gray-400">
-                <span class="text-sm">No patterns in this playlist</span>
-            </div>
-        `;
-        return;
-    }
-
-    // Clear grid and add all pattern cards
-    patternsGrid.innerHTML = '';
-
-    patterns.forEach(pattern => {
-        const patternCard = createPatternCard(pattern, true);
-        patternsGrid.appendChild(patternCard);
-        patternCard.dataset.pattern = pattern;
-
-        // Set up lazy loading for patterns outside viewport
-        intersectionObserver.observe(patternCard);
-    });
-
-    // After DOM is updated, immediately load previews for visible patterns
-    // Use requestAnimationFrame to ensure DOM layout is complete
-    requestAnimationFrame(() => {
-        setTimeout(() => {
-            loadVisiblePlaylistPreviews();
-        }, 50); // Small delay to ensure grid layout is complete
-    });
-}
-
-// Load previews for patterns currently visible in the playlist
-async function loadVisiblePlaylistPreviews() {
-    const visiblePatterns = new Map();
-    const patternCards = document.querySelectorAll('#patternsGrid [data-pattern]');
-    
-    patternCards.forEach(card => {
-        const pattern = card.dataset.pattern;
-        const previewContainer = card.querySelector('.pattern-preview');
-        
-        // Skip if pattern is already displayed (has an img element) or if already in memory cache
-        if (!pattern || previewCache.has(pattern) || previewContainer.querySelector('img')) return;
-        
-        // Check if card is visible in viewport
-        const rect = card.getBoundingClientRect();
-        const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
-        
-        if (isVisible) {
-            visiblePatterns.set(pattern, card);
-            // Remove from intersection observer since we're loading it immediately
-            intersectionObserver.unobserve(card);
-        }
-    });
-    
-    if (visiblePatterns.size > 0) {
-        logMessage(`Loading ${visiblePatterns.size} visible playlist previews not found in cache`, LOG_TYPE.DEBUG);
-        
-        // Add visible patterns to pending batch
-        for (const [pattern, element] of visiblePatterns) {
-            pendingPatterns.set(pattern, element);
-        }
-        
-        // Process batch immediately for visible patterns
-        await processPendingBatch();
-    }
-}
-
-// Create a pattern card
-function createPatternCard(pattern, showRemove = false) {
-    const card = document.createElement('div');
-    card.className = 'flex flex-col gap-3 group cursor-pointer relative';
-
-    const previewContainer = document.createElement('div');
-    previewContainer.className = 'w-full aspect-square bg-cover rounded-full shadow-sm group-hover:shadow-md transition-shadow duration-150 border border-gray-200 dark:border-gray-700 pattern-preview relative';
-
-    // Check in-memory cache first
-    const previewData = previewCache.get(pattern);
-    if (previewData && !previewData.error && previewData.image_data) {
-        previewContainer.innerHTML = `<img src="${previewData.image_data}" alt="Pattern Preview" class="w-full h-full object-cover rounded-full" />`;
-    } else {
-        // Try to load from IndexedDB cache asynchronously
-        loadPreviewFromCache(pattern, previewContainer);
-    }
-
-    const patternName = document.createElement('p');
-    patternName.className = 'text-sm text-gray-800 dark:text-gray-300 group-hover:text-gray-900 dark:group-hover:text-gray-100 font-medium truncate text-center';
-    patternName.textContent = pattern.replace('.thr', '').split('/').pop();
-
-    card.appendChild(previewContainer);
-    card.appendChild(patternName);
-
-    if (showRemove) {
-        const removeBtn = document.createElement('button');
-        removeBtn.className = 'absolute top-2 right-2 size-6 rounded-full bg-red-500 hover:bg-red-600 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-150 flex items-center justify-center text-xs';
-        removeBtn.innerHTML = '<span class="material-icons text-sm">close</span>';
-        removeBtn.addEventListener('click', (e) => {
-            e.stopPropagation();
-            removePatternFromPlaylist(pattern);
-        });
-        card.appendChild(removeBtn);
-    }
-
-    return card;
-}
-
-// Load preview from IndexedDB cache and update the preview container
-async function loadPreviewFromCache(pattern, previewContainer) {
-    try {
-        const cachedData = await getPreviewFromCache(pattern);
-        if (cachedData && !cachedData.error && cachedData.image_data) {
-            // Add to in-memory cache for faster future access
-            previewCache.set(pattern, cachedData);
-            // Update the preview container
-            if (previewContainer && !previewContainer.querySelector('img')) {
-                previewContainer.innerHTML = `<img src="${cachedData.image_data}" alt="Pattern Preview" class="w-full h-full object-cover rounded-full" />`;
-            }
-        }
-    } catch (error) {
-        logMessage(`Error loading preview from cache for ${pattern}: ${error.message}`, LOG_TYPE.WARNING);
-    }
-}
-
-// Sort patterns by specified field and direction
-function sortPatterns(patterns, sortField, sortDirection) {
-    return patterns.sort((a, b) => {
-        let aVal, bVal;
-        
-        switch (sortField) {
-            case 'name':
-                aVal = a.name.toLowerCase();
-                bVal = b.name.toLowerCase();
-                break;
-            case 'date':
-                aVal = a.date_modified;
-                bVal = b.date_modified;
-                break;
-            case 'coordinates':
-                aVal = a.coordinates_count;
-                bVal = b.coordinates_count;
-                break;
-            case 'favorite':
-                // Check if patterns are in favorites (access global favoritePatterns)
-                const aIsFavorite = window.favoritePatterns ? window.favoritePatterns.has(a.path) : false;
-                const bIsFavorite = window.favoritePatterns ? window.favoritePatterns.has(b.path) : false;
-                
-                if (aIsFavorite && !bIsFavorite) return sortDirection === 'asc' ? -1 : 1;
-                if (!aIsFavorite && bIsFavorite) return sortDirection === 'asc' ? 1 : -1;
-                
-                // Both have same favorite status, sort by name as secondary sort
-                aVal = a.name.toLowerCase();
-                bVal = b.name.toLowerCase();
-                break;
-            default:
-                aVal = a.name.toLowerCase();
-                bVal = b.name.toLowerCase();
-        }
-        
-        let result = 0;
-        if (aVal < bVal) result = -1;
-        else if (aVal > bVal) result = 1;
-        
-        return sortDirection === 'asc' ? result : -result;
-    });
-}
-
-// Filter patterns based on current filters
-function filterPatterns(patterns, filters, searchQuery = '') {
-    return patterns.filter(pattern => {
-        // Category filter
-        if (filters.category !== 'all' && pattern.category !== filters.category) {
-            return false;
-        }
-        
-        // Search query filter
-        if (searchQuery.trim()) {
-            const normalizedQuery = searchQuery.toLowerCase().trim();
-            const patternName = pattern.name.toLowerCase();
-            const category = pattern.category.toLowerCase();
-            return patternName.includes(normalizedQuery) || category.includes(normalizedQuery);
-        }
-        
-        return true;
-    });
-}
-
-// Apply sorting and filtering to patterns
-function applyPatternsFilteringAndSorting() {
-    const searchQuery = document.getElementById('patternSearchInput')?.value || '';
-    
-    // Check if enhanced metadata is available
-    if (!availablePatternsWithMetadata || availablePatternsWithMetadata.length === 0) {
-        // Fallback to basic search if metadata not loaded yet
-        if (searchQuery.trim()) {
-            filteredPatterns = availablePatterns.filter(pattern => 
-                pattern.toLowerCase().includes(searchQuery.toLowerCase())
-            );
-        } else {
-            filteredPatterns = [...availablePatterns];
-        }
-        displayAvailablePatterns();
-        return;
-    }
-    
-    // Start with all available patterns with metadata
-    let patterns = [...availablePatternsWithMetadata];
-    
-    // Apply filters
-    patterns = filterPatterns(patterns, currentFilters, searchQuery);
-    
-    // Apply sorting
-    patterns = sortPatterns(patterns, currentSort.field, currentSort.direction);
-    
-    // Update filtered patterns (convert back to path format for compatibility)
-    filteredPatterns = patterns.map(p => p.path);
-    
-    // Update display
-    displayAvailablePatterns();
-    updateSortAndFilterUI();
-}
-
-// Search and filter patterns (updated to work with metadata)
-function searchPatterns(query) {
-    applyPatternsFilteringAndSorting();
-}
-
-// Update sort and filter UI to reflect current state
-function updateSortAndFilterUI() {
-    // Update sort direction icon
-    const sortDirectionIcon = document.getElementById('sortDirectionIcon');
-    if (sortDirectionIcon) {
-        sortDirectionIcon.textContent = currentSort.direction === 'asc' ? 'arrow_upward' : 'arrow_downward';
-    }
-    
-    // Update sort field select
-    const sortFieldSelect = document.getElementById('sortFieldSelect');
-    if (sortFieldSelect) {
-        sortFieldSelect.value = currentSort.field;
-    }
-    
-    // Update filter selects
-    const categorySelect = document.getElementById('categoryFilterSelect');
-    if (categorySelect) {
-        categorySelect.value = currentFilters.category;
-    }
-}
-
-// Populate category filter dropdown with available categories (subfolders)
-function updateCategoryFilter() {
-    const categorySelect = document.getElementById('categoryFilterSelect');
-    if (!categorySelect) return;
-    
-    // Check if metadata is available
-    if (!availablePatternsWithMetadata || availablePatternsWithMetadata.length === 0) {
-        // Show basic options if metadata not loaded
-        categorySelect.innerHTML = '<option value="all">All Folders (loading...)</option>';
-        return;
-    }
-    
-    // Get unique categories (subfolders)
-    const categories = [...new Set(availablePatternsWithMetadata.map(p => p.category))].sort();
-    
-    // Clear existing options except "All"
-    categorySelect.innerHTML = '<option value="all">All Folders</option>';
-    
-    // Add category options
-    categories.forEach(category => {
-        if (category) {
-            const option = document.createElement('option');
-            option.value = category;
-            // Display friendly names for full paths
-            if (category === 'root') {
-                option.textContent = 'Root Folder';
-            } else {
-                // For full paths, show the path but make it more readable
-                const displayName = category
-                    .split('/')
-                    .map(part => part.charAt(0).toUpperCase() + part.slice(1).replace('_', ' '))
-                    .join(' › ');
-                option.textContent = displayName;
-            }
-            categorySelect.appendChild(option);
-        }
-    });
-}
-
-// Handle sort field change
-function handleSortFieldChange() {
-    const sortFieldSelect = document.getElementById('sortFieldSelect');
-    if (sortFieldSelect) {
-        currentSort.field = sortFieldSelect.value;
-        applyPatternsFilteringAndSorting();
-    }
-}
-
-// Handle sort direction toggle
-function handleSortDirectionToggle() {
-    currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc';
-    applyPatternsFilteringAndSorting();
-}
-
-// Handle category filter change
-function handleCategoryFilterChange() {
-    const categorySelect = document.getElementById('categoryFilterSelect');
-    if (categorySelect) {
-        currentFilters.category = categorySelect.value;
-        applyPatternsFilteringAndSorting();
-    }
-}
-
-
-// Handle search input
-function handleSearchInput() {
-    const searchInput = document.getElementById('patternSearchInput');
-    const clearBtn = document.getElementById('clearSearchBtn');
-    const query = searchInput.value;
-    
-    // Show/hide clear button
-    if (query) {
-        clearBtn.classList.remove('hidden');
-    } else {
-        clearBtn.classList.add('hidden');
-    }
-    
-    // Debounce search
-    if (searchTimeout) {
-        clearTimeout(searchTimeout);
-    }
-    
-    searchTimeout = setTimeout(() => {
-        searchPatterns(query);
-    }, 300);
-}
-
-// Clear search
-function clearSearch() {
-    const searchInput = document.getElementById('patternSearchInput');
-    const clearBtn = document.getElementById('clearSearchBtn');
-    
-    searchInput.value = '';
-    clearBtn.classList.add('hidden');
-    searchPatterns('');
-}
-
-// Remove pattern from playlist
-async function removePatternFromPlaylist(pattern) {
-    if (!currentPlaylist) return;
-
-    if (confirm(`Remove "${pattern.split('/').pop()}" from playlist?`)) {
-        try {
-            // Get current playlist data
-            const response = await fetch(`/get_playlist?name=${encodeURIComponent(currentPlaylist)}`);
-            if (response.ok) {
-                const playlistData = await response.json();
-                const updatedFiles = playlistData.files.filter(file => file !== pattern);
-                
-                // Update playlist
-                const updateResponse = await fetch('/modify_playlist', {
-                    method: 'POST',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({
-                        playlist_name: currentPlaylist,
-                        files: updatedFiles
-                    })
-                });
-
-                if (updateResponse.ok) {
-                    showStatusMessage('Pattern removed from playlist', 'success');
-                    await loadPlaylistPatterns(currentPlaylist);
-                } else {
-                    throw new Error('Failed to update playlist');
-                }
-            }
-        } catch (error) {
-            logMessage(`Error removing pattern: ${error.message}`, LOG_TYPE.ERROR);
-            showStatusMessage('Failed to remove pattern', 'error');
-        }
-    }
-}
-
-// Load available patterns for adding (with metadata for sorting/filtering)
-async function loadAvailablePatterns(forceRefresh = false) {
-    const loadingIndicator = document.getElementById('patternsLoadingIndicator');
-    const grid = document.getElementById('availablePatternsGrid');
-    const noResultsMessage = document.getElementById('noResultsMessage');
-    
-    // Always fetch from backend
-    loadingIndicator.classList.remove('hidden');
-    grid.classList.add('hidden');
-    noResultsMessage.classList.add('hidden');
-    
-    try {
-        // First load basic patterns list for fast initial display
-        logMessage('Fetching basic patterns list from server', LOG_TYPE.DEBUG);
-        const patterns = await getCachedPatternFiles(forceRefresh);
-        const thrPatterns = patterns.filter(file => file.endsWith('.thr'));
-        availablePatterns = [...thrPatterns];
-        filteredPatterns = [...availablePatterns];
-        
-        // Show patterns immediately for fast loading
-        displayAvailablePatterns();
-        
-        // Then load metadata in background
-        setTimeout(async () => {
-            try {
-                logMessage('Loading enhanced metadata in background', LOG_TYPE.DEBUG);
-                const metadataResponse = await fetch('/list_theta_rho_files_with_metadata');
-                if (metadataResponse.ok) {
-                    const patternsWithMetadata = await metadataResponse.json();
-                    availablePatternsWithMetadata = [...patternsWithMetadata];
-                    
-                    // Update category filter dropdown now that we have metadata
-                    updateCategoryFilter();
-                    
-                    logMessage(`Enhanced metadata loaded for ${patternsWithMetadata.length} patterns`, LOG_TYPE.DEBUG);
-                } else {
-                    logMessage('Failed to load enhanced metadata - using basic functionality', LOG_TYPE.WARNING);
-                }
-            } catch (metadataError) {
-                logMessage(`Error loading enhanced metadata: ${metadataError.message}`, LOG_TYPE.WARNING);
-                // Continue with basic functionality
-            }
-        }, 100);
-        
-        if (forceRefresh) {
-            showStatusMessage('Patterns list refreshed successfully', 'success');
-        }
-    } catch (error) {
-        logMessage(`Error loading available patterns: ${error.message}`, LOG_TYPE.ERROR);
-        showStatusMessage('Failed to load available patterns', 'error');
-    } finally {
-        loadingIndicator.classList.add('hidden');
-    }
-}
-
-// Update selection count display
-function updateSelectionCount() {
-    const countElement = document.getElementById('selectionCount');
-    if (countElement) {
-        const count = selectedPatterns.size;
-        countElement.textContent = `${count} selected`;
-    }
-    updateToggleSelectAllButton();
-}
-
-// Smart toggle for Select All / Deselect All
-function toggleSelectAll() {
-    const patterns = filteredPatterns.length > 0 ? filteredPatterns : availablePatterns;
-    const allSelected = patterns.length > 0 && patterns.every(pattern => selectedPatterns.has(pattern));
-    
-    if (allSelected) {
-        // Deselect all
-        selectedPatterns.clear();
-    } else {
-        // Select all
-        patterns.forEach(pattern => {
-            selectedPatterns.add(pattern);
-        });
-    }
-    
-    updatePatternSelection();
-    updateSelectionCount();
-}
-
-// Update the toggle button text and icon based on selection state
-function updateToggleSelectAllButton() {
-    const patterns = filteredPatterns.length > 0 ? filteredPatterns : availablePatterns;
-    const allSelected = patterns.length > 0 && patterns.every(pattern => selectedPatterns.has(pattern));
-    
-    const icon = document.getElementById('toggleSelectAllIcon');
-    const text = document.getElementById('toggleSelectAllText');
-    
-    if (icon && text) {
-        if (allSelected) {
-            icon.textContent = 'check_box';
-            text.textContent = 'Deselect All';
-        } else {
-            icon.textContent = 'check_box_outline_blank';
-            text.textContent = 'Select All';
-        }
-    }
-}
-
-// Select all visible patterns (legacy function - keep for compatibility)
-function selectAllPatterns() {
-    const patterns = filteredPatterns.length > 0 ? filteredPatterns : availablePatterns;
-    patterns.forEach(pattern => {
-        selectedPatterns.add(pattern);
-    });
-    updatePatternSelection();
-    updateSelectionCount();
-}
-
-// Deselect all patterns (legacy function - keep for compatibility)
-function deselectAllPatterns() {
-    selectedPatterns.clear();
-    updatePatternSelection();
-    updateSelectionCount();
-}
-
-// Update visual selection state for all pattern cards
-function updatePatternSelection() {
-    const cards = document.querySelectorAll('#availablePatternsGrid .group');
-    cards.forEach(card => {
-        const patternName = card.dataset.pattern;
-        
-        if (selectedPatterns.has(patternName)) {
-            card.classList.add('ring-2', 'ring-blue-500');
-        } else {
-            card.classList.remove('ring-2', 'ring-blue-500');
-        }
-    });
-}
-
-// Display available patterns in modal
-function displayAvailablePatterns() {
-    const grid = document.getElementById('availablePatternsGrid');
-    const noResultsMessage = document.getElementById('noResultsMessage');
-    
-    grid.classList.remove('hidden');
-    noResultsMessage.classList.add('hidden');
-    grid.innerHTML = '';
-
-    if (filteredPatterns.length === 0) {
-        grid.classList.add('hidden');
-        noResultsMessage.classList.remove('hidden');
-        return;
-    }
-
-    filteredPatterns.forEach((pattern) => {
-        const card = document.createElement('div');
-        const isSelected = selectedPatterns.has(pattern);
-        
-        // Add blue ring if already selected
-        card.className = `group flex flex-col gap-2 cursor-pointer transition-all duration-150 hover:scale-105 ${isSelected ? 'ring-2 ring-blue-500' : ''}`;
-        card.dataset.pattern = pattern;
-        
-        card.innerHTML = `
-            <div class="w-full bg-center aspect-square bg-cover rounded-full border border-gray-200 dark:border-gray-700 relative pattern-preview">
-            </div>
-            <p class="text-xs text-gray-700 dark:text-gray-300 font-medium truncate text-center">${pattern.replace('.thr', '').split('/').pop()}</p>
-        `;
-
-        const previewContainer = card.querySelector('.pattern-preview');
-        
-        // Check in-memory cache first
-        const previewData = previewCache.get(pattern);
-        if (previewData && !previewData.error && previewData.image_data) {
-            previewContainer.innerHTML = `<img src="${previewData.image_data}" alt="Pattern Preview" class="w-full h-full object-cover rounded-full" />`;
-        } else {
-            // Try to load from IndexedDB cache asynchronously
-            loadPreviewFromCache(pattern, previewContainer);
-        }
-        
-        // Set up lazy loading for ALL patterns
-        intersectionObserver.observe(card);
-
-        // Handle selection
-        card.addEventListener('click', () => {
-            if (selectedPatterns.has(pattern)) {
-                selectedPatterns.delete(pattern);
-                card.classList.remove('ring-2', 'ring-blue-500');
-            } else {
-                selectedPatterns.add(pattern);
-                card.classList.add('ring-2', 'ring-blue-500');
-            }
-            updateSelectionCount();
-        });
-
-        grid.appendChild(card);
-    });
-    
-    // Trigger immediate preview loading for visible patterns in modal
-    requestAnimationFrame(() => {
-        setTimeout(() => {
-            loadVisibleModalPreviews();
-        }, 50); // Small delay to ensure modal layout is complete
-    });
-}
-
-// Load previews for patterns currently visible in the modal
-async function loadVisibleModalPreviews() {
-    const visiblePatterns = new Map();
-    const patternCards = document.querySelectorAll('#availablePatternsGrid [data-pattern]');
-    
-    patternCards.forEach(card => {
-        const pattern = card.dataset.pattern;
-        const previewContainer = card.querySelector('.pattern-preview');
-        
-        // Skip if pattern is already displayed (has an img element) or if already in memory cache
-        if (!pattern || previewCache.has(pattern) || previewContainer.querySelector('img')) return;
-        
-        // Check if card is visible in viewport
-        const rect = card.getBoundingClientRect();
-        const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
-        
-        if (isVisible) {
-            visiblePatterns.set(pattern, card);
-            // Remove from intersection observer since we're loading it immediately
-            intersectionObserver.unobserve(card);
-        }
-    });
-    
-    if (visiblePatterns.size > 0) {
-        logMessage(`Loading ${visiblePatterns.size} visible modal previews not found in cache`, LOG_TYPE.DEBUG);
-        
-        // Add visible patterns to pending batch
-        for (const [pattern, element] of visiblePatterns) {
-            pendingPatterns.set(pattern, element);
-        }
-        
-        // Process batch immediately for visible patterns
-        await processPendingBatch();
-    }
-}
-
-
-// Add pattern to pending batch for efficient loading
-async function addPatternToBatch(pattern, element) {
-    // Check in-memory cache first
-    if (previewCache.has(pattern)) {
-        const previewData = previewCache.get(pattern);
-        if (previewData && !previewData.error) {
-            if (element) {
-                updatePreviewElement(element, previewData.image_data);
-            }
-        }
-        return;
-    }
-
-    // Check IndexedDB cache
-    const cachedData = await getPreviewFromCache(pattern);
-    if (cachedData && !cachedData.error) {
-        // Add to in-memory cache for faster access
-        previewCache.set(pattern, cachedData);
-        if (element) {
-            updatePreviewElement(element, cachedData.image_data);
-        }
-        return;
-    }
-    
-    // Add loading indicator with better styling
-    if (element && !element.querySelector('img')) {
-        element.innerHTML = `
-            <div class="absolute inset-0 flex items-center justify-center bg-slate-100 rounded-full">
-                <div class="bg-slate-200 rounded-full h-8 w-8 flex items-center justify-center">
-                    <div class="bg-slate-500 rounded-full h-4 w-4"></div>
-                </div>
-            </div>
-            <div class="absolute inset-0 flex items-center justify-center">
-                <div class="text-xs text-slate-500 mt-12">Loading...</div>
-            </div>
-        `;
-    }
-
-    // Add to pending batch
-    pendingPatterns.set(pattern, element);
-    
-    // Process batch immediately if it's full
-    if (pendingPatterns.size >= BATCH_SIZE) {
-        processPendingBatch();
-    }
-}
-
-// Update preview element with image
-function updatePreviewElement(element, imageData) {
-    if (element) {
-        element.innerHTML = `<img src="${imageData}" alt="Pattern Preview" class="w-full h-full object-cover rounded-full" />`;
-        
-        // Re-add the add button if it exists in the parent card
-        const card = element.closest('[data-pattern]');
-        if (card && !selectedPatterns.has(card.dataset.pattern)) {
-            const addBtnContainer = document.createElement('div');
-            addBtnContainer.className = 'absolute top-2 right-2 size-6 rounded-full bg-white dark:bg-gray-700 shadow-md opacity-0 transition-opacity duration-150 flex items-center justify-center';
-            addBtnContainer.innerHTML = '<span class="material-icons text-sm text-gray-600 dark:text-gray-300">add</span>';
-            element.appendChild(addBtnContainer);
-        }
-    }
-}
-
-// Save selected patterns to playlist (replaces entire playlist)
-async function addSelectedPatternsToPlaylist() {
-    if (!currentPlaylist) return;
-
-    try {
-        // Simply replace the playlist with the selected patterns
-        const updatedFiles = Array.from(selectedPatterns);
-        
-        // Update playlist
-        const updateResponse = await fetch('/modify_playlist', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({
-                playlist_name: currentPlaylist,
-                files: updatedFiles
-            })
-        });
-
-        if (updateResponse.ok) {
-            showStatusMessage(`Playlist "${currentPlaylist}" saved successfully`, 'success');
-            selectedPatterns.clear();
-            document.getElementById('addPatternsModal').classList.add('hidden');
-            await loadPlaylistPatterns(currentPlaylist);
-        } else {
-            throw new Error('Failed to update playlist');
-        }
-    } catch (error) {
-        logMessage(`Error saving playlist: ${error.message}`, LOG_TYPE.ERROR);
-        showStatusMessage('Failed to save playlist', 'error');
-    }
-}
-
-// Run playlist
-async function runPlaylist() {
-    if (!currentPlaylist) return;
-
-    const runMode = document.querySelector('input[name="run_playlist"]:checked')?.value || 'single';
-    const pauseTime = parseInt(document.getElementById('pauseTimeInput').value) || 0;
-    const clearPattern = document.getElementById('clearPatternSelect').value;
-    const shuffle = document.getElementById('shuffleCheckbox')?.checked || false;
-
-    // Check if a pattern is currently running and show stopping message
-    if (window.currentPlaybackStatus?.is_running) {
-        showStatusMessage('Stopping current pattern...', 'info');
-    }
-
-    try {
-        const response = await fetch('/run_playlist', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({
-                playlist_name: currentPlaylist,
-                run_mode: runMode,
-                pause_time: pauseTime,
-                clear_pattern: clearPattern === 'none' ? null : clearPattern,
-                shuffle: shuffle
-            })
-        });
-
-        if (response.ok) {
-            showStatusMessage(`Started playlist: ${currentPlaylist}`, 'success');
-            // Show the preview modal when a playlist starts
-            if (typeof setModalVisibility === 'function') {
-                setModalVisibility(true, false);
-            } else if (window.openPlayerPreviewModal) {
-                window.openPlayerPreviewModal();
-            }
-        } else {
-            let errorMsg = 'Failed to run playlist';
-            let errorType = 'error';
-            
-            try {
-                const data = await response.json();
-                if (data.detail) {
-                    errorMsg = data.detail;
-                    
-                    // Handle specific error cases with appropriate messaging
-                    if (data.detail === 'Connection not established') {
-                        errorMsg = 'Please connect to the device before running a playlist';
-                        errorType = 'warning';
-                    } else if (response.status === 409) {
-                        errorMsg = 'Another pattern is already running. Please stop the current pattern first.';
-                        errorType = 'warning';
-                    } else if (response.status === 404) {
-                        errorMsg = 'Playlist not found. Please refresh the page and try again.';
-                        errorType = 'error';
-                    }
-                }
-            } catch (e) {
-                // If we can't parse the JSON, use status-based messaging
-                if (response.status === 400) {
-                    errorMsg = 'Invalid request. Please check your settings and try again.';
-                } else if (response.status === 500) {
-                    errorMsg = 'Server error. Please try again later.';
-                }
-            }
-            
-            showStatusMessage(errorMsg, errorType);
-        }
-    } catch (error) {
-        logMessage(`Error running playlist: ${error.message}`, LOG_TYPE.ERROR);
-        
-        // Handle network errors specifically
-        if (error.name === 'TypeError' && error.message.includes('fetch')) {
-            showStatusMessage('Network error. Please check your connection and try again.', 'error');
-        } else {
-            showStatusMessage('Failed to run playlist', 'error');
-        }
-    }
-}
-
-// Create new playlist
-async function createNewPlaylist() {
-    const playlistName = document.getElementById('newPlaylistName').value.trim();
-    
-    if (!playlistName) {
-        showStatusMessage('Please enter a playlist name', 'warning');
-        return;
-    }
-
-    try {
-        const response = await fetch('/create_playlist', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({
-                playlist_name: playlistName,
-                files: []
-            })
-        });
-
-        if (response.ok) {
-            showStatusMessage('Playlist created successfully', 'success');
-            document.getElementById('addPlaylistModal').classList.add('hidden');
-            document.getElementById('newPlaylistName').value = '';
-            await loadPlaylists();
-        } else {
-            const data = await response.json();
-            throw new Error(data.detail || 'Failed to create playlist');
-        }
-    } catch (error) {
-        logMessage(`Error creating playlist: ${error.message}`, LOG_TYPE.ERROR);
-        showStatusMessage('Failed to create playlist', 'error');
-    }
-}
-
-// Delete playlist
-async function deletePlaylist(playlistName) {
-    if (!confirm(`Are you sure you want to delete the playlist "${playlistName}"?`)) {
-        return;
-    }
-
-    try {
-        const response = await fetch('/delete_playlist', {
-            method: 'DELETE',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({
-                playlist_name: playlistName
-            })
-        });
-
-        if (response.ok) {
-            showStatusMessage(`Playlist "${playlistName}" deleted successfully`, 'success');
-            
-            // If the deleted playlist was selected, clear the selection
-            if (currentPlaylist === playlistName) {
-                currentPlaylist = null;
-                const header = document.getElementById('currentPlaylistTitle');
-                header.innerHTML = '<h1 class="text-gray-900 text-2xl font-semibold leading-tight truncate">Select a Playlist</h1>';
-                document.getElementById('addPatternsBtn').disabled = true;
-                document.getElementById('runPlaylistBtn').disabled = true;
-                document.getElementById('playbackSettings').classList.add('hidden');
-                document.getElementById('patternsGrid').innerHTML = `
-                    <div class="flex items-center justify-center col-span-full py-12 text-gray-500 dark:text-gray-400">
-                        <span class="text-sm text-center">Select a playlist to view its patterns</span>
-                    </div>
-                `;
-                
-                // Return to playlists list on mobile
-                showPlaylistsList();
-            }
-            
-            // Reload playlists
-            await loadPlaylists();
-        } else {
-            const data = await response.json();
-            throw new Error(data.detail || 'Failed to delete playlist');
-        }
-    } catch (error) {
-        logMessage(`Error deleting playlist: ${error.message}`, LOG_TYPE.ERROR);
-        showStatusMessage('Failed to delete playlist', 'error');
-    }
-}
-
-// Open rename modal
-function openRenameModal(playlistName) {
-    const modal = document.getElementById('renamePlaylistModal');
-    const input = document.getElementById('renamePlaylistInput');
-
-    // Set the current name
-    input.value = playlistName;
-    input.dataset.oldName = playlistName;
-
-    // Show modal
-    modal.classList.remove('hidden');
-
-    // Focus and select input
-    const focusInput = () => {
-        input.focus();
-        input.select();
-    };
-
-    focusInput();
-    requestAnimationFrame(focusInput);
-    setTimeout(focusInput, 50);
-}
-
-// Close rename modal
-function closeRenameModal() {
-    const modal = document.getElementById('renamePlaylistModal');
-    const input = document.getElementById('renamePlaylistInput');
-
-    modal.classList.add('hidden');
-    input.value = '';
-    delete input.dataset.oldName;
-}
-
-// Rename playlist
-async function renamePlaylist() {
-    const input = document.getElementById('renamePlaylistInput');
-    const oldName = input.dataset.oldName;
-    const newName = input.value.trim();
-
-    if (!newName) {
-        showStatusMessage('Please enter a playlist name', 'warning');
-        return;
-    }
-
-    if (newName === oldName) {
-        closeRenameModal();
-        return;
-    }
-
-    try {
-        const response = await fetch('/rename_playlist', {
-            method: 'POST',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({
-                old_name: oldName,
-                new_name: newName
-            })
-        });
-
-        if (response.ok) {
-            const data = await response.json();
-            showStatusMessage(`Playlist renamed to "${newName}"`, 'success');
-            closeRenameModal();
-
-            // Update current playlist reference
-            if (currentPlaylist === oldName) {
-                currentPlaylist = newName;
-
-                // Update last selected playlist
-                saveLastSelectedPlaylist(newName);
-            }
-
-            // Reload playlists and reselect
-            await loadPlaylists();
-
-            // Find and click the renamed playlist using data attribute
-            setTimeout(() => {
-                const nav = document.getElementById('playlistsNav');
-                const el = nav.querySelector(`a[data-playlist-name="${newName}"]`);
-                if (el) el.click();
-            }, 100);
-        } else {
-            const data = await response.json();
-            throw new Error(data.detail || 'Failed to rename playlist');
-        }
-    } catch (error) {
-        logMessage(`Error renaming playlist: ${error.message}`, LOG_TYPE.ERROR);
-        showStatusMessage(error.message || 'Failed to rename playlist', 'error');
-    }
-}
-
-// Setup event listeners
-function setupEventListeners() {
-    // Mobile back button event listeners
-    document.getElementById('mobileBackBtn').addEventListener('click', () => {
-        showPlaylistsList();
-    });
-
-    // Add playlist button
-    document.getElementById('addPlaylistBtn').addEventListener('click', () => {
-        const modal = document.getElementById('addPlaylistModal');
-        const input = document.getElementById('newPlaylistName');
-        
-        // Show modal first
-        modal.classList.remove('hidden');
-        
-        // Focus handling
-        const focusInput = () => {
-            if (input) {
-                input.focus();
-                input.select();
-            }
-        };
-
-        // Try multiple approaches to ensure focus
-        focusInput();
-        requestAnimationFrame(focusInput);
-        setTimeout(focusInput, 50);
-        setTimeout(focusInput, 100);
-    });
-
-    // Add patterns button
-    document.getElementById('addPatternsBtn').addEventListener('click', () => {
-        // Update modal title immediately
-        const modalTitle = document.getElementById('modalTitle');
-        if (modalTitle) {
-            modalTitle.textContent = currentPlaylist ? `Edit Patterns for "${currentPlaylist}"` : 'Add Patterns to Playlist';
-        }
-        
-        // Show modal immediately for better responsiveness
-        document.getElementById('addPatternsModal').classList.remove('hidden');
-        
-        // Focus search input when modal opens
-        setTimeout(() => {
-            document.getElementById('patternSearchInput').focus();
-        }, 100);
-        
-        // Load data asynchronously after modal is visible
-        const loadModalData = async () => {
-            try {
-                // Load current playlist patterns first if editing
-                if (currentPlaylist) {
-                    const response = await fetch(`/get_playlist?name=${encodeURIComponent(currentPlaylist)}`);
-                    if (response.ok) {
-                        const playlistData = await response.json();
-                        const currentFiles = playlistData.files || [];
-                        // Pre-select current patterns
-                        selectedPatterns.clear();
-                        currentFiles.forEach(pattern => selectedPatterns.add(pattern));
-                    }
-                }
-                
-                // Load available patterns
-                await loadAvailablePatterns();
-                updatePatternSelection();
-                updateSelectionCount();
-            } catch (error) {
-                console.error('Failed to load modal data:', error);
-                showStatusMessage('Failed to load patterns', 'error');
-            }
-        };
-        
-        // Start loading data without blocking
-        loadModalData();
-    });
-
-    // Search functionality
-    document.getElementById('patternSearchInput').addEventListener('input', handleSearchInput);
-    document.getElementById('clearSearchBtn').addEventListener('click', clearSearch);
-    
-    // Sort and filter controls
-    document.getElementById('sortFieldSelect').addEventListener('change', handleSortFieldChange);
-    document.getElementById('sortDirectionBtn').addEventListener('click', handleSortDirectionToggle);
-    document.getElementById('categoryFilterSelect').addEventListener('change', handleCategoryFilterChange);
-    
-    // Handle Enter key in search input
-    document.getElementById('patternSearchInput').addEventListener('keypress', (e) => {
-        if (e.key === 'Enter') {
-            e.preventDefault();
-        }
-    });
-    
-    // Run playlist button
-    document.getElementById('runPlaylistBtn').addEventListener('click', runPlaylist);
-
-    // Modal controls
-    document.getElementById('cancelPlaylistBtn').addEventListener('click', () => {
-        document.getElementById('addPlaylistModal').classList.add('hidden');
-    });
-
-    document.getElementById('createPlaylistBtn').addEventListener('click', createNewPlaylist);
-
-    document.getElementById('cancelAddPatternsBtn').addEventListener('click', () => {
-        selectedPatterns.clear();
-        clearSearch();
-        updateSelectionCount();
-        document.getElementById('addPatternsModal').classList.add('hidden');
-    });
-
-    document.getElementById('confirmAddPatternsBtn').addEventListener('click', addSelectedPatternsToPlaylist);
-    
-    // Smart Toggle Select All button
-    const toggleSelectBtn = document.getElementById('toggleSelectAllBtn');
-    if (toggleSelectBtn) {
-        toggleSelectBtn.addEventListener('click', toggleSelectAll);
-    }
-
-    // Handle Enter key in new playlist name input
-    document.getElementById('newPlaylistName').addEventListener('keypress', (e) => {
-        if (e.key === 'Enter') {
-            createNewPlaylist();
-        }
-    });
-
-    // Rename modal event listeners
-    document.getElementById('cancelRenameBtn').addEventListener('click', closeRenameModal);
-    document.getElementById('confirmRenameBtn').addEventListener('click', renamePlaylist);
-    document.getElementById('renamePlaylistInput').addEventListener('keypress', (e) => {
-        if (e.key === 'Enter') {
-            renamePlaylist();
-        }
-    });
-
-    // Close modals when clicking outside
-    document.getElementById('addPlaylistModal').addEventListener('click', (e) => {
-        if (e.target.id === 'addPlaylistModal') {
-            document.getElementById('addPlaylistModal').classList.add('hidden');
-        }
-    });
-
-    document.getElementById('addPatternsModal').addEventListener('click', (e) => {
-        if (e.target.id === 'addPatternsModal') {
-            selectedPatterns.clear();
-            clearSearch();
-            document.getElementById('addPatternsModal').classList.add('hidden');
-        }
-    });
-
-    document.getElementById('renamePlaylistModal').addEventListener('click', (e) => {
-        if (e.target.id === 'renamePlaylistModal') {
-            closeRenameModal();
-        }
-    });
-}
-
-// Initialize playlists page
-document.addEventListener('DOMContentLoaded', () => {
-    try {
-        // Initialize UI immediately for fast page load
-        initializeIntersectionObserver();
-        setupEventListeners();
-        
-        // Initialize mobile view state
-        isMobileView = isMobile();
-        if (isMobileView) {
-            initMobileLayout();
-        } else {
-            initDesktopLayout();
-        }
-        
-        // Add window resize listener for responsive behavior
-        window.addEventListener('resize', updateMobileView);
-        
-        // Restore playback settings
-        restorePlaybackSettings();
-        setupPlaybackSettingsPersistence();
-        
-        // Show loading indicator for playlists
-        const playlistsNav = document.getElementById('playlistsNav');
-        if (playlistsNav) {
-            playlistsNav.innerHTML = `
-                <div class="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400">
-                    <div class="flex items-center gap-2">
-                        <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-gray-500"></div>
-                        <span class="text-sm">Loading playlists...</span>
-                    </div>
-                </div>
-            `;
-        }
-        
-        // Load data asynchronously without blocking page render
-        Promise.all([
-            // Initialize IndexedDB preview cache
-            initPreviewCacheDB().catch(err => {
-                console.error('Failed to init preview cache:', err);
-                return null;
-            }),
-            
-            // Load playlists
-            loadPlaylists().catch(err => {
-                console.error('Failed to load playlists:', err);
-                showStatusMessage('Failed to load playlists', 'error');
-                return null;
-            }),
-            
-            // Check serial connection status
-            checkSerialStatus().catch(err => {
-                console.error('Failed to check serial status:', err);
-                return null;
-            })
-        ]).then(() => {
-            logMessage('Playlists page initialized successfully', LOG_TYPE.SUCCESS);
-        }).catch(error => {
-            logMessage(`Error during initialization: ${error.message}`, LOG_TYPE.ERROR);
-            showStatusMessage('Failed to initialize playlists page', 'error');
-        });
-        
-    } catch (error) {
-        logMessage(`Error during initialization: ${error.message}`, LOG_TYPE.ERROR);
-        showStatusMessage('Failed to initialize playlists page', 'error');
-    }
-});
-
-// Check serial connection status
-async function checkSerialStatus() {
-    try {
-        const response = await fetch('/serial_status');
-        if (response.ok) {
-            const data = await response.json();
-            const statusDot = document.getElementById('connectionStatusDot');
-            if (statusDot) {
-                statusDot.className = `inline-block size-2 rounded-full ml-2 align-middle ${
-                    data.connected ? 'bg-green-500' : 'bg-red-500'
-                }`;
-            }
-        }
-    } catch (error) {
-        logMessage(`Error checking serial status: ${error.message}`, LOG_TYPE.ERROR);
-    }
-}
-
-// Mobile utility functions
-function isMobile() {
-    return window.innerWidth <= 768;
-}
-
-function updateMobileView() {
-    const wasMobile = isMobileView;
-    isMobileView = isMobile();
-    
-    if (wasMobile !== isMobileView) {
-        // Mobile state changed, update layout
-        if (isMobileView) {
-            initMobileLayout();
-        } else {
-            initDesktopLayout();
-        }
-    }
-}
-
-function initMobileLayout() {
-    const sidebar = document.getElementById('playlistsSidebar');
-    const details = document.getElementById('playlistDetails');
-    const mobileBackBtn = document.getElementById('mobileBackBtn');
-    
-    if (!currentPlaylist) {
-        // Show playlists list, hide details
-        sidebar.classList.remove('mobile-hidden');
-        details.classList.add('mobile-hidden');
-        mobileBackBtn.classList.add('mobile-hidden');
-    } else {
-        // Show details, hide playlists list
-        sidebar.classList.add('mobile-hidden');
-        details.classList.remove('mobile-hidden');
-        mobileBackBtn.classList.remove('mobile-hidden');
-        mobileBackBtn.classList.add('mobile-flex');
-    }
-}
-
-function initDesktopLayout() {
-    const sidebar = document.getElementById('playlistsSidebar');
-    const details = document.getElementById('playlistDetails');
-    const mobileBackBtn = document.getElementById('mobileBackBtn');
-    
-    // Show both sidebar and details on desktop
-    sidebar.classList.remove('mobile-hidden');
-    details.classList.remove('mobile-hidden');
-    mobileBackBtn.classList.add('mobile-hidden');
-    mobileBackBtn.classList.remove('mobile-flex');
-}
-
-function showPlaylistDetails() {
-    if (isMobileView) {
-        const sidebar = document.getElementById('playlistsSidebar');
-        const details = document.getElementById('playlistDetails');
-        const mobileBackBtn = document.getElementById('mobileBackBtn');
-        
-        sidebar.classList.add('mobile-hidden');
-        details.classList.remove('mobile-hidden');
-        mobileBackBtn.classList.remove('mobile-hidden');
-        mobileBackBtn.classList.add('mobile-flex');
-    }
-}
-
-function showPlaylistsList() {
-    if (isMobileView) {
-        const sidebar = document.getElementById('playlistsSidebar');
-        const details = document.getElementById('playlistDetails');
-        const mobileBackBtn = document.getElementById('mobileBackBtn');
-        
-        sidebar.classList.remove('mobile-hidden');
-        details.classList.add('mobile-hidden');
-        mobileBackBtn.classList.add('mobile-hidden');
-        mobileBackBtn.classList.remove('mobile-flex');
-    }
-} 

+ 0 - 2439
static/js/settings.js

@@ -1,2439 +0,0 @@
-// ============================================================================
-// 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');
-    }
-}
-
-// Initialize section headers with proper event listeners for cross-browser compatibility
-// This fixes Firefox issues where inline onclick handlers may not work reliably
-// with user-select: none and flexbox layouts
-function initializeSectionHeaders() {
-    const sectionHeaders = document.querySelectorAll('.section-header');
-    sectionHeaders.forEach(header => {
-        // Remove inline onclick to prevent double-firing
-        header.removeAttribute('onclick');
-
-        // Add click event listener (more reliable than inline onclick in Firefox)
-        header.addEventListener('click', function(e) {
-            // Prevent text selection on double-click
-            e.preventDefault();
-            toggleSection(this);
-        });
-
-        // Also handle keyboard accessibility
-        header.setAttribute('role', 'button');
-        header.setAttribute('tabindex', '0');
-        header.addEventListener('keydown', function(e) {
-            if (e.key === 'Enter' || e.key === ' ') {
-                e.preventDefault();
-                toggleSection(this);
-            }
-        });
-    });
-}
-
-// Initialize on DOM ready
-document.addEventListener('DOMContentLoaded', initializeSectionHeaders);
-
-// ============================================================================
-// Constants and Utilities
-// ============================================================================
-
-// Constants for log message types
-const LOG_TYPE = {
-    SUCCESS: 'success',
-    WARNING: 'warning',
-    ERROR: 'error',
-    INFO: 'info',
-    DEBUG: 'debug'
-};
-
-// Helper function to convert provider name to camelCase for ID lookup
-// e.g., "dw_leds" -> "DwLeds", "wled" -> "Wled", "none" -> "None"
-function providerToCamelCase(provider) {
-    return provider.split('_').map(word =>
-        word.charAt(0).toUpperCase() + word.slice(1)
-    ).join('');
-}
-
-// Constants for cache
-const CACHE_KEYS = {
-    CONNECTION_STATUS: 'connection_status',
-    LAST_UPDATE: 'last_status_update'
-};
-const CACHE_DURATION = 5000; // 5 seconds cache duration
-
-// Function to log messages
-function logMessage(message, type = LOG_TYPE.DEBUG) {
-    console.log(`[${type}] ${message}`);
-}
-
-// Function to get cached connection status
-function getCachedConnectionStatus() {
-    const cachedData = localStorage.getItem(CACHE_KEYS.CONNECTION_STATUS);
-    const lastUpdate = localStorage.getItem(CACHE_KEYS.LAST_UPDATE);
-    
-    if (cachedData && lastUpdate) {
-        const now = Date.now();
-        const cacheAge = now - parseInt(lastUpdate);
-        
-        if (cacheAge < CACHE_DURATION) {
-            return JSON.parse(cachedData);
-        }
-    }
-    return null;
-}
-
-// Function to set cached connection status
-function setCachedConnectionStatus(data) {
-    localStorage.setItem(CACHE_KEYS.CONNECTION_STATUS, JSON.stringify(data));
-    localStorage.setItem(CACHE_KEYS.LAST_UPDATE, Date.now().toString());
-}
-
-// Function to update serial connection status
-async function updateSerialStatus(forceUpdate = false) {
-    try {
-        // Check cache first unless force update is requested
-        if (!forceUpdate) {
-            const cachedData = getCachedConnectionStatus();
-            if (cachedData) {
-                updateConnectionUI(cachedData);
-                return;
-            }
-        }
-
-        const response = await fetch('/serial_status');
-        if (response.ok) {
-            const data = await response.json();
-            setCachedConnectionStatus(data);
-            updateConnectionUI(data);
-        }
-    } catch (error) {
-        logMessage(`Error checking serial status: ${error.message}`, LOG_TYPE.ERROR);
-    }
-}
-
-// Function to update UI based on connection status
-function updateConnectionUI(data) {
-    const statusElement = document.getElementById('serialStatus');
-    const iconElement = document.querySelector('.material-icons.text-3xl');
-    const disconnectButton = document.getElementById('disconnectButton');
-    const portSelectionDiv = document.getElementById('portSelectionDiv');
-    
-    if (statusElement && iconElement) {
-        if (data.connected) {
-            statusElement.textContent = `Connected to ${data.port || 'unknown port'}`;
-            statusElement.className = 'text-green-500 text-sm font-medium leading-normal';
-            iconElement.textContent = 'usb';
-            if (disconnectButton) {
-                disconnectButton.hidden = false;
-            }
-            if (portSelectionDiv) {
-                portSelectionDiv.hidden = true;
-            }
-        } else {
-            statusElement.textContent = 'Disconnected';
-            statusElement.className = 'text-red-500 text-sm font-medium leading-normal';
-            iconElement.textContent = 'usb_off';
-            if (disconnectButton) {
-                disconnectButton.hidden = true;
-            }
-            if (portSelectionDiv) {
-                portSelectionDiv.hidden = false;
-            }
-        }
-    }
-}
-
-// Function to update available serial ports
-async function updateSerialPorts() {
-    try {
-        const response = await fetch('/list_serial_ports');
-        if (response.ok) {
-            const ports = await response.json();
-            const portsElement = document.getElementById('availablePorts');
-            const portSelect = document.getElementById('portSelect');
-            const preferredPortSelect = document.getElementById('preferredPortSelect');
-
-            if (portsElement) {
-                portsElement.textContent = ports.length > 0 ? ports.join(', ') : 'No ports available';
-            }
-
-            if (portSelect) {
-                // Clear existing options except the first one
-                while (portSelect.options.length > 1) {
-                    portSelect.remove(1);
-                }
-
-                // Add new options
-                ports.forEach(port => {
-                    const option = document.createElement('option');
-                    option.value = port;
-                    option.textContent = port;
-                    portSelect.appendChild(option);
-                });
-
-                // If there's exactly one port available, select and connect to it
-                if (ports.length === 1) {
-                    portSelect.value = ports[0];
-                    // Trigger connect button click
-                    const connectButton = document.getElementById('connectButton');
-                    if (connectButton) {
-                        connectButton.click();
-                    }
-                }
-            }
-
-            // Also update the preferred port select dropdown
-            if (preferredPortSelect) {
-                // Store current selection
-                const currentPreferred = preferredPortSelect.value;
-
-                // Clear existing options except the first one (no preference)
-                while (preferredPortSelect.options.length > 1) {
-                    preferredPortSelect.remove(1);
-                }
-
-                // Add all available ports
-                ports.forEach(port => {
-                    const option = document.createElement('option');
-                    option.value = port;
-                    option.textContent = port;
-                    preferredPortSelect.appendChild(option);
-                });
-
-                // Restore selection if it's still available
-                if (currentPreferred && ports.includes(currentPreferred)) {
-                    preferredPortSelect.value = currentPreferred;
-                }
-            }
-        }
-    } catch (error) {
-        logMessage(`Error fetching serial ports: ${error.message}`, LOG_TYPE.ERROR);
-    }
-}
-
-// Function to load and display preferred port setting
-async function loadPreferredPort() {
-    try {
-        const response = await fetch('/api/preferred-port');
-        if (response.ok) {
-            const data = await response.json();
-            const preferredPortSelect = document.getElementById('preferredPortSelect');
-            const currentPreferredPort = document.getElementById('currentPreferredPort');
-            const preferredPortDisplay = document.getElementById('preferredPortDisplay');
-
-            if (preferredPortSelect && data.preferred_port) {
-                // Check if the preferred port is in the options
-                const optionExists = Array.from(preferredPortSelect.options).some(
-                    opt => opt.value === data.preferred_port
-                );
-
-                if (optionExists) {
-                    preferredPortSelect.value = data.preferred_port;
-                } else {
-                    // Add the preferred port as an option (it might not be currently available)
-                    const option = document.createElement('option');
-                    option.value = data.preferred_port;
-                    option.textContent = `${data.preferred_port} (not currently available)`;
-                    preferredPortSelect.appendChild(option);
-                    preferredPortSelect.value = data.preferred_port;
-                }
-            }
-
-            // Show current preferred port indicator
-            if (currentPreferredPort && preferredPortDisplay && data.preferred_port) {
-                preferredPortDisplay.textContent = `Currently set to: ${data.preferred_port}`;
-                currentPreferredPort.classList.remove('hidden');
-            } else if (currentPreferredPort) {
-                currentPreferredPort.classList.add('hidden');
-            }
-        }
-    } catch (error) {
-        logMessage(`Error loading preferred port: ${error.message}`, LOG_TYPE.ERROR);
-    }
-}
-
-// Function to save preferred port setting
-async function savePreferredPort() {
-    const preferredPortSelect = document.getElementById('preferredPortSelect');
-    if (!preferredPortSelect) return;
-
-    const preferredPort = preferredPortSelect.value || null;
-
-    try {
-        const response = await fetch('/api/settings', {
-            method: 'PATCH',
-            headers: { 'Content-Type': 'application/json' },
-            body: JSON.stringify({ connection: { preferred_port: preferredPort } })
-        });
-
-        if (response.ok) {
-            await response.json();
-            const currentPreferredPort = document.getElementById('currentPreferredPort');
-            const preferredPortDisplay = document.getElementById('preferredPortDisplay');
-
-            if (preferredPort) {
-                showStatusMessage(`Preferred port set to: ${preferredPort}`, 'success');
-                if (currentPreferredPort && preferredPortDisplay) {
-                    preferredPortDisplay.textContent = `Currently set to: ${preferredPort}`;
-                    currentPreferredPort.classList.remove('hidden');
-                }
-            } else {
-                showStatusMessage('Preferred port cleared - will auto-detect on startup', 'success');
-                if (currentPreferredPort) {
-                    currentPreferredPort.classList.add('hidden');
-                }
-            }
-        } else {
-            throw new Error('Failed to save preferred port');
-        }
-    } catch (error) {
-        showStatusMessage(`Failed to save preferred port: ${error.message}`, 'error');
-    }
-}
-
-function setWledButtonState(isSet) {
-    const saveWledConfig = document.getElementById('saveWledConfig');
-    if (!saveWledConfig) return;
-    if (isSet) {
-        saveWledConfig.className = 'flex items-center justify-center gap-2 min-w-[100px] max-w-[480px] cursor-pointer rounded-lg h-10 px-4 bg-red-600 hover:bg-red-700 text-white text-sm font-medium leading-normal tracking-[0.015em] transition-colors';
-        saveWledConfig.innerHTML = '<span class="material-icons text-lg">close</span><span class="truncate">Clear WLED IP</span>';
-    } else {
-        saveWledConfig.className = 'flex items-center justify-center gap-2 min-w-[100px] max-w-[480px] 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';
-        saveWledConfig.innerHTML = '<span class="material-icons text-lg">save</span><span class="truncate">Save Configuration</span>';
-    }
-}
-
-// Handle LED provider selection and show/hide appropriate config sections
-function updateLedProviderUI() {
-    const provider = document.querySelector('input[name="ledProvider"]:checked')?.value || 'none';
-    const wledConfig = document.getElementById('wledConfig');
-    const dwLedsConfig = document.getElementById('dwLedsConfig');
-
-    if (wledConfig && dwLedsConfig) {
-        if (provider === 'wled') {
-            wledConfig.classList.remove('hidden');
-            dwLedsConfig.classList.add('hidden');
-        } else if (provider === 'dw_leds') {
-            wledConfig.classList.add('hidden');
-            dwLedsConfig.classList.remove('hidden');
-        } else {
-            wledConfig.classList.add('hidden');
-            dwLedsConfig.classList.add('hidden');
-        }
-    }
-}
-
-// Load LED configuration from server
-async function loadLedConfig() {
-    try {
-        const response = await fetch('/get_led_config');
-        if (response.ok) {
-            const data = await response.json();
-
-            // Set provider radio button
-            const providerRadio = document.getElementById(`ledProvider${providerToCamelCase(data.provider)}`);
-            if (providerRadio) {
-                providerRadio.checked = true;
-            } else {
-                document.getElementById('ledProviderNone').checked = true;
-            }
-
-            // Set WLED IP if configured
-            if (data.wled_ip) {
-                const wledIpInput = document.getElementById('wledIpInput');
-                if (wledIpInput) {
-                    wledIpInput.value = data.wled_ip;
-                }
-            }
-
-            // Set DW LED configuration if configured
-            if (data.dw_led_num_leds) {
-                const numLedsInput = document.getElementById('dwLedNumLeds');
-                if (numLedsInput) {
-                    numLedsInput.value = data.dw_led_num_leds;
-                }
-            }
-            if (data.dw_led_gpio_pin) {
-                const gpioPinInput = document.getElementById('dwLedGpioPin');
-                if (gpioPinInput) {
-                    gpioPinInput.value = data.dw_led_gpio_pin;
-                }
-            }
-            if (data.dw_led_pixel_order) {
-                const pixelOrderInput = document.getElementById('dwLedPixelOrder');
-                if (pixelOrderInput) {
-                    pixelOrderInput.value = data.dw_led_pixel_order;
-                }
-            }
-
-            // Update UI to show correct config section
-            updateLedProviderUI();
-        }
-    } catch (error) {
-        logMessage(`Error loading LED config: ${error.message}`, LOG_TYPE.ERROR);
-    }
-}
-
-// Initialize settings page
-document.addEventListener('DOMContentLoaded', async () => {
-    // Initialize UI with default disconnected state
-    updateConnectionUI({ connected: false });
-
-    // Handle scroll to section if hash is present in URL
-    if (window.location.hash) {
-        setTimeout(() => {
-            const targetSection = document.querySelector(window.location.hash);
-            if (targetSection) {
-                targetSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
-                // Add a subtle highlight animation
-                targetSection.style.transition = 'background-color 0.5s ease';
-                const originalBg = targetSection.style.backgroundColor;
-                targetSection.style.backgroundColor = 'rgba(14, 165, 233, 0.1)';
-                setTimeout(() => {
-                    targetSection.style.backgroundColor = originalBg;
-                }, 2000);
-            }
-        }, 300); // Delay to ensure page is fully loaded
-    }
-    
-    // Load all data asynchronously using unified settings endpoint
-    Promise.all([
-        // Unified settings endpoint (replaces multiple individual fetches)
-        fetch('/api/settings').then(response => response.json()).catch(() => ({})),
-
-        // Non-settings operational endpoints (kept separate)
-        fetch('/serial_status').then(response => response.json()).catch(() => ({ connected: false })),
-        fetch('/api/version').then(response => response.json()).catch(() => ({ current: '1.0.0', latest: '1.0.0', update_available: false })),
-        fetch('/list_serial_ports').then(response => response.json()).catch(() => []),
-        getCachedPatternFiles().catch(() => [])
-    ]).then(([settings, statusData, updateData, ports, patterns]) => {
-        // Map unified settings to legacy variable names for backward compatibility with existing UI code
-        const ledConfigData = {
-            provider: settings.led?.provider || 'none',
-            wled_ip: settings.led?.wled_ip || null,
-            dw_led_num_leds: settings.led?.dw_led?.num_leds,
-            dw_led_gpio_pin: settings.led?.dw_led?.gpio_pin,
-            dw_led_pixel_order: settings.led?.dw_led?.pixel_order
-        };
-        const clearPatterns = {
-            custom_clear_from_in: settings.patterns?.custom_clear_from_in,
-            custom_clear_from_out: settings.patterns?.custom_clear_from_out
-        };
-        const clearSpeedData = {
-            clear_pattern_speed: settings.patterns?.clear_pattern_speed,
-            effective_speed: settings.patterns?.clear_pattern_speed // Will be handled by UI
-        };
-        const appNameData = { app_name: settings.app?.name || 'Dune Weaver' };
-        const scheduledPauseData = settings.scheduled_pause || { enabled: false, time_slots: [] };
-        const preferredPortData = { preferred_port: settings.connection?.preferred_port };
-
-        // Store full settings for other initialization functions
-        window.unifiedSettings = settings;
-        // Update connection status
-        setCachedConnectionStatus(statusData);
-        updateConnectionUI(statusData);
-
-        // Update LED configuration
-        const providerRadio = document.getElementById(`ledProvider${providerToCamelCase(ledConfigData.provider)}`);
-        if (providerRadio) {
-            providerRadio.checked = true;
-        } else {
-            document.getElementById('ledProviderNone').checked = true;
-        }
-
-        if (ledConfigData.wled_ip) {
-            const wledIpInput = document.getElementById('wledIpInput');
-            if (wledIpInput) wledIpInput.value = ledConfigData.wled_ip;
-        }
-
-        // Load DW LED settings
-        if (ledConfigData.dw_led_num_leds) {
-            const numLedsInput = document.getElementById('dwLedNumLeds');
-            if (numLedsInput) numLedsInput.value = ledConfigData.dw_led_num_leds;
-        }
-        if (ledConfigData.dw_led_gpio_pin) {
-            const gpioPinInput = document.getElementById('dwLedGpioPin');
-            if (gpioPinInput) gpioPinInput.value = ledConfigData.dw_led_gpio_pin;
-        }
-        if (ledConfigData.dw_led_pixel_order) {
-            const pixelOrderInput = document.getElementById('dwLedPixelOrder');
-            if (pixelOrderInput) pixelOrderInput.value = ledConfigData.dw_led_pixel_order;
-        }
-
-        updateLedProviderUI()
-        
-        // Update version display
-        const currentVersionText = document.getElementById('currentVersionText');
-        const latestVersionText = document.getElementById('latestVersionText');
-        const updateButton = document.getElementById('updateSoftware');
-        const updateIcon = document.getElementById('updateIcon');
-        const updateText = document.getElementById('updateText');
-        
-        if (currentVersionText) {
-            currentVersionText.textContent = updateData.current;
-        }
-        
-        if (latestVersionText) {
-            if (updateData.error) {
-                latestVersionText.textContent = 'Error checking updates';
-                latestVersionText.className = 'text-red-500 text-sm font-normal leading-normal';
-            } else {
-                latestVersionText.textContent = updateData.latest;
-                latestVersionText.className = 'text-slate-500 text-sm font-normal leading-normal';
-            }
-        }
-        
-        // Update button state
-        if (updateButton && updateIcon && updateText) {
-            if (updateData.update_available) {
-                updateButton.disabled = false;
-                updateButton.className = 'flex items-center justify-center gap-1.5 min-w-[84px] cursor-pointer rounded-lg h-9 px-3 bg-emerald-500 hover:bg-emerald-600 text-white text-xs font-medium leading-normal tracking-[0.015em] transition-colors';
-                updateIcon.textContent = 'download';
-                updateText.textContent = 'Update';
-            } else {
-                updateButton.disabled = true;
-                updateButton.className = 'flex items-center justify-center gap-1.5 min-w-[84px] cursor-pointer rounded-lg h-9 px-3 bg-gray-400 text-white text-xs font-medium leading-normal tracking-[0.015em] transition-colors disabled:opacity-50 disabled:cursor-not-allowed';
-                updateIcon.textContent = 'check';
-                updateText.textContent = 'Up to date';
-            }
-        }
-        
-        // Update port selection
-        const portSelect = document.getElementById('portSelect');
-        if (portSelect) {
-            // Clear existing options except the first one
-            while (portSelect.options.length > 1) {
-                portSelect.remove(1);
-            }
-
-            // Add new options
-            ports.forEach(port => {
-                const option = document.createElement('option');
-                option.value = port;
-                option.textContent = port;
-                portSelect.appendChild(option);
-            });
-
-            // If there's exactly one port available, select it
-            if (ports.length === 1) {
-                portSelect.value = ports[0];
-            }
-        }
-
-        // Update preferred port selection
-        const preferredPortSelect = document.getElementById('preferredPortSelect');
-        const currentPreferredPort = document.getElementById('currentPreferredPort');
-        const preferredPortDisplay = document.getElementById('preferredPortDisplay');
-
-        if (preferredPortSelect) {
-            // Clear existing options except the first one (no preference)
-            while (preferredPortSelect.options.length > 1) {
-                preferredPortSelect.remove(1);
-            }
-
-            // Add all available ports
-            ports.forEach(port => {
-                const option = document.createElement('option');
-                option.value = port;
-                option.textContent = port;
-                preferredPortSelect.appendChild(option);
-            });
-
-            // Set the current preferred port value
-            if (preferredPortData && preferredPortData.preferred_port) {
-                // Check if the preferred port is in the available ports
-                const isAvailable = ports.includes(preferredPortData.preferred_port);
-
-                if (isAvailable) {
-                    preferredPortSelect.value = preferredPortData.preferred_port;
-                } else {
-                    // Add the preferred port as an option (it might not be currently available)
-                    const option = document.createElement('option');
-                    option.value = preferredPortData.preferred_port;
-                    option.textContent = `${preferredPortData.preferred_port} (not currently available)`;
-                    preferredPortSelect.appendChild(option);
-                    preferredPortSelect.value = preferredPortData.preferred_port;
-                }
-
-                // Show the current preferred port indicator
-                if (currentPreferredPort && preferredPortDisplay) {
-                    preferredPortDisplay.textContent = `Currently set to: ${preferredPortData.preferred_port}`;
-                    currentPreferredPort.classList.remove('hidden');
-                }
-            }
-        }
-
-        // Initialize autocomplete for clear patterns
-        const clearFromInInput = document.getElementById('customClearFromInInput');
-        const clearFromOutInput = document.getElementById('customClearFromOutInput');
-        
-        if (clearFromInInput && clearFromOutInput && patterns && Array.isArray(patterns)) {
-            // Store patterns globally for autocomplete
-            window.availablePatterns = patterns;
-            
-            // Set current values if they exist
-            if (clearPatterns && clearPatterns.custom_clear_from_in) {
-                clearFromInInput.value = clearPatterns.custom_clear_from_in;
-            }
-            if (clearPatterns && clearPatterns.custom_clear_from_out) {
-                clearFromOutInput.value = clearPatterns.custom_clear_from_out;
-            }
-            
-            // Initialize autocomplete for both inputs
-            initializeAutocomplete('customClearFromInInput', 'clearFromInSuggestions', 'clearFromInClear', patterns);
-            initializeAutocomplete('customClearFromOutInput', 'clearFromOutSuggestions', 'clearFromOutClear', patterns);
-            
-            console.log('Autocomplete initialized with', patterns.length, 'patterns');
-        }
-        
-        // Set clear pattern speed
-        const clearPatternSpeedInput = document.getElementById('clearPatternSpeedInput');
-        const effectiveClearSpeed = document.getElementById('effectiveClearSpeed');
-        if (clearPatternSpeedInput && clearSpeedData) {
-            // Only set value if clear_pattern_speed is not null
-            if (clearSpeedData.clear_pattern_speed !== null && clearSpeedData.clear_pattern_speed !== undefined) {
-                clearPatternSpeedInput.value = clearSpeedData.clear_pattern_speed;
-                if (effectiveClearSpeed) {
-                    effectiveClearSpeed.textContent = `Current: ${clearSpeedData.clear_pattern_speed} steps/min`;
-                }
-            } else {
-                // Leave empty to show placeholder for default
-                clearPatternSpeedInput.value = '';
-                if (effectiveClearSpeed && clearSpeedData.effective_speed) {
-                    effectiveClearSpeed.textContent = `Using default pattern speed: ${clearSpeedData.effective_speed} steps/min`;
-                }
-            }
-        }
-        
-        // Update app name
-        const appNameInput = document.getElementById('appNameInput');
-        if (appNameInput && appNameData.app_name) {
-            appNameInput.value = appNameData.app_name;
-        }
-
-        // Store Still Sands data for later initialization
-        window.initialStillSandsData = scheduledPauseData;
-    }).catch(error => {
-        logMessage(`Error initializing settings page: ${error.message}`, LOG_TYPE.ERROR);
-    });
-
-    // Set up event listeners
-    setupEventListeners();
-});
-
-// Setup event listeners
-function setupEventListeners() {
-    // Save App Name
-    const saveAppNameButton = document.getElementById('saveAppName');
-    const appNameInput = document.getElementById('appNameInput');
-    if (saveAppNameButton && appNameInput) {
-        saveAppNameButton.addEventListener('click', async () => {
-            const appName = appNameInput.value.trim() || 'Dune Weaver';
-
-            try {
-                const response = await fetch('/api/settings', {
-                    method: 'PATCH',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({ app: { name: appName } })
-                });
-
-                if (response.ok) {
-                    await response.json();
-                    showStatusMessage('Application name updated successfully. Refresh the page to see changes.', 'success');
-
-                    // Update the page title and header immediately
-                    document.title = `Settings - ${appName}`;
-                    const headerTitle = document.querySelector('h1.text-gray-800');
-                    if (headerTitle) {
-                        // Update just the text content, preserving the connection status dot
-                        const textNode = headerTitle.childNodes[0];
-                        if (textNode && textNode.nodeType === Node.TEXT_NODE) {
-                            textNode.textContent = data.app_name;
-                        }
-                    }
-                } else {
-                    throw new Error('Failed to save application name');
-                }
-            } catch (error) {
-                showStatusMessage(`Failed to save application name: ${error.message}`, 'error');
-            }
-        });
-        
-        // Handle Enter key in app name input
-        appNameInput.addEventListener('keypress', (e) => {
-            if (e.key === 'Enter') {
-                saveAppNameButton.click();
-            }
-        });
-    }
-    
-    // LED provider selection change handlers
-    const ledProviderRadios = document.querySelectorAll('input[name="ledProvider"]');
-    ledProviderRadios.forEach(radio => {
-        radio.addEventListener('change', updateLedProviderUI);
-    });
-
-    // Save LED configuration
-    const saveLedConfig = document.getElementById('saveLedConfig');
-    if (saveLedConfig) {
-        saveLedConfig.addEventListener('click', async () => {
-            const provider = document.querySelector('input[name="ledProvider"]:checked')?.value || 'none';
-
-            let requestBody = { provider };
-
-            if (provider === 'wled') {
-                const wledIp = document.getElementById('wledIpInput')?.value;
-                if (!wledIp) {
-                    showStatusMessage('Please enter a WLED IP address', 'error');
-                    return;
-                }
-                requestBody.ip_address = wledIp;
-            } else if (provider === 'dw_leds') {
-                const numLeds = parseInt(document.getElementById('dwLedNumLeds')?.value) || 60;
-                const gpioPin = parseInt(document.getElementById('dwLedGpioPin')?.value) || 12;
-                const pixelOrder = document.getElementById('dwLedPixelOrder')?.value || 'GRB';
-
-                requestBody.num_leds = numLeds;
-                requestBody.gpio_pin = gpioPin;
-                requestBody.pixel_order = pixelOrder;
-            }
-
-            try {
-                const response = await fetch('/set_led_config', {
-                    method: 'POST',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify(requestBody)
-                });
-
-                if (response.ok) {
-                    const data = await response.json();
-
-                    if (provider === 'wled' && data.wled_ip) {
-                        localStorage.setItem('wled_ip', data.wled_ip);
-                        showStatusMessage('WLED configured successfully', 'success');
-                    } else if (provider === 'dw_leds') {
-                        // Check if there's a warning (hardware not available but settings saved)
-                        if (data.warning) {
-                            showStatusMessage(
-                                `Settings saved for testing. Hardware issue: ${data.warning}`,
-                                'warning'
-                            );
-                        } else {
-                            showStatusMessage(
-                                `DW LEDs configured: ${data.dw_led_num_leds} LEDs on GPIO${data.dw_led_gpio_pin}`,
-                                'success'
-                            );
-                        }
-                    } else if (provider === 'none') {
-                        localStorage.removeItem('wled_ip');
-                        showStatusMessage('LED controller disabled', 'success');
-                    }
-                } else {
-                    // Extract error detail from response
-                    const errorData = await response.json().catch(() => ({}));
-                    const errorMessage = errorData.detail || 'Failed to save LED configuration';
-                    showStatusMessage(errorMessage, 'error');
-                }
-            } catch (error) {
-                showStatusMessage(`Failed to save LED configuration: ${error.message}`, 'error');
-            }
-        });
-    }
-
-    // Update software
-    const updateSoftware = document.getElementById('updateSoftware');
-    if (updateSoftware) {
-        updateSoftware.addEventListener('click', async () => {
-            if (updateSoftware.disabled) {
-                return;
-            }
-            
-            try {
-                const response = await fetch('/api/update', {
-                    method: 'POST'
-                });
-                const data = await response.json();
-                
-                if (data.success) {
-                    showStatusMessage('Software update started successfully', 'success');
-                } else if (data.manual_update_url) {
-                    // Show modal with manual update instructions, but use wiki link
-                    const wikiData = {
-                        ...data,
-                        manual_update_url: 'https://github.com/tuanchris/dune-weaver/wiki/Updating-software'
-                    };
-                    showUpdateInstructionsModal(wikiData);
-                } else {
-                    showStatusMessage(data.message || 'No updates available', 'info');
-                }
-            } catch (error) {
-                logMessage(`Error updating software: ${error.message}`, LOG_TYPE.ERROR);
-                showStatusMessage('Failed to check for updates', 'error');
-            }
-        });
-    }
-
-    // Connect button
-    const connectButton = document.getElementById('connectButton');
-    if (connectButton) {
-        connectButton.addEventListener('click', async () => {
-            const portSelect = document.getElementById('portSelect');
-            if (!portSelect || !portSelect.value) {
-                logMessage('Please select a port first', LOG_TYPE.WARNING);
-                return;
-            }
-
-            try {
-                const response = await fetch('/connect', {
-                    method: 'POST',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({ port: portSelect.value })
-                });
-                
-                if (response.ok) {
-                    logMessage('Connected successfully', LOG_TYPE.SUCCESS);
-                    await updateSerialStatus(true); // Force update after connecting
-                } else {
-                    throw new Error('Failed to connect');
-                }
-            } catch (error) {
-                logMessage(`Error connecting to device: ${error.message}`, LOG_TYPE.ERROR);
-            }
-        });
-    }
-
-    // Disconnect button
-    const disconnectButton = document.getElementById('disconnectButton');
-    if (disconnectButton) {
-        disconnectButton.addEventListener('click', async () => {
-            try {
-                const response = await fetch('/disconnect', {
-                    method: 'POST'
-                });
-                if (response.ok) {
-                    logMessage('Device disconnected successfully', LOG_TYPE.SUCCESS);
-                    await updateSerialStatus(true); // Force update after disconnecting
-                } else {
-                    throw new Error('Failed to disconnect device');
-                }
-            } catch (error) {
-                logMessage(`Error disconnecting device: ${error.message}`, LOG_TYPE.ERROR);
-            }
-        });
-    }
-
-    // Save preferred port button
-    const savePreferredPortButton = document.getElementById('savePreferredPort');
-    if (savePreferredPortButton) {
-        savePreferredPortButton.addEventListener('click', savePreferredPort);
-    }
-
-    // Save custom clear patterns button
-    const saveClearPatterns = document.getElementById('saveClearPatterns');
-    if (saveClearPatterns) {
-        saveClearPatterns.addEventListener('click', async () => {
-            const clearFromInInput = document.getElementById('customClearFromInInput');
-            const clearFromOutInput = document.getElementById('customClearFromOutInput');
-            
-            if (!clearFromInInput || !clearFromOutInput) {
-                return;
-            }
-            
-            // Validate that the entered patterns exist (if not empty)
-            const inValue = clearFromInInput.value.trim();
-            const outValue = clearFromOutInput.value.trim();
-            
-            if (inValue && window.availablePatterns && !window.availablePatterns.includes(inValue)) {
-                showStatusMessage(`Pattern not found: ${inValue}`, 'error');
-                return;
-            }
-            
-            if (outValue && window.availablePatterns && !window.availablePatterns.includes(outValue)) {
-                showStatusMessage(`Pattern not found: ${outValue}`, 'error');
-                return;
-            }
-            
-            try {
-                const response = await fetch('/api/settings', {
-                    method: 'PATCH',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({
-                        patterns: {
-                            custom_clear_from_in: inValue || null,
-                            custom_clear_from_out: outValue || null
-                        }
-                    })
-                });
-
-                if (response.ok) {
-                    showStatusMessage('Clear patterns saved successfully', 'success');
-                } else {
-                    const error = await response.json();
-                    throw new Error(error.detail || 'Failed to save clear patterns');
-                }
-            } catch (error) {
-                showStatusMessage(`Failed to save clear patterns: ${error.message}`, 'error');
-            }
-        });
-    }
-    
-    // Logo upload functionality
-    const logoFileInput = document.getElementById('logoFileInput');
-    const resetLogoBtn = document.getElementById('resetLogoBtn');
-    const logoPreview = document.getElementById('logoPreview');
-    const logoUploadStatus = document.getElementById('logoUploadStatus');
-
-    if (logoFileInput) {
-        logoFileInput.addEventListener('change', async (event) => {
-            const file = event.target.files[0];
-            if (!file) return;
-
-            // Show uploading status
-            if (logoUploadStatus) {
-                logoUploadStatus.textContent = 'Uploading...';
-                logoUploadStatus.className = 'text-xs text-slate-500';
-            }
-
-            const formData = new FormData();
-            formData.append('file', file);
-
-            try {
-                const response = await fetch('/api/upload-logo', {
-                    method: 'POST',
-                    body: formData
-                });
-
-                if (response.ok) {
-                    const data = await response.json();
-
-                    // Update preview image with cache-busting query param
-                    if (logoPreview) {
-                        logoPreview.src = data.url + '?t=' + Date.now();
-                    }
-
-                    // Show reset button
-                    if (resetLogoBtn) {
-                        resetLogoBtn.classList.remove('hidden');
-                    }
-
-                    // Show success status
-                    if (logoUploadStatus) {
-                        logoUploadStatus.textContent = 'Logo uploaded successfully!';
-                        logoUploadStatus.className = 'text-xs text-green-600';
-                        setTimeout(() => {
-                            logoUploadStatus.textContent = '';
-                        }, 3000);
-                    }
-
-                    showStatusMessage('Logo uploaded successfully. Refresh the page to see all changes.', 'success');
-                } else {
-                    const error = await response.json();
-                    throw new Error(error.detail || 'Failed to upload logo');
-                }
-            } catch (error) {
-                if (logoUploadStatus) {
-                    logoUploadStatus.textContent = `Error: ${error.message}`;
-                    logoUploadStatus.className = 'text-xs text-red-600';
-                }
-                showStatusMessage(`Failed to upload logo: ${error.message}`, 'error');
-            }
-
-            // Reset file input so the same file can be re-selected
-            logoFileInput.value = '';
-        });
-    }
-
-    if (resetLogoBtn) {
-        resetLogoBtn.addEventListener('click', async () => {
-            try {
-                const response = await fetch('/api/custom-logo', {
-                    method: 'DELETE'
-                });
-
-                if (response.ok) {
-                    // Reset preview to default logo
-                    if (logoPreview) {
-                        logoPreview.src = '/static/apple-touch-icon.png?t=' + Date.now();
-                    }
-
-                    // Hide reset button
-                    resetLogoBtn.classList.add('hidden');
-
-                    showStatusMessage('Logo reset to default. Refresh the page to see all changes.', 'success');
-                } else {
-                    const error = await response.json();
-                    throw new Error(error.detail || 'Failed to reset logo');
-                }
-            } catch (error) {
-                showStatusMessage(`Failed to reset logo: ${error.message}`, 'error');
-            }
-        });
-    }
-
-    // Save clear pattern speed button
-    const saveClearSpeed = document.getElementById('saveClearSpeed');
-    if (saveClearSpeed) {
-        saveClearSpeed.addEventListener('click', async () => {
-            const clearPatternSpeedInput = document.getElementById('clearPatternSpeedInput');
-            
-            if (!clearPatternSpeedInput) {
-                return;
-            }
-            
-            let speed;
-            if (clearPatternSpeedInput.value === '' || clearPatternSpeedInput.value === null) {
-                // Empty value means use default (None)
-                speed = null;
-            } else {
-                speed = parseInt(clearPatternSpeedInput.value);
-                
-                // Validate speed only if it's not null
-                if (isNaN(speed) || speed < 50 || speed > 2000) {
-                    showStatusMessage('Clear pattern speed must be between 50 and 2000, or leave empty for default', 'error');
-                    return;
-                }
-            }
-            
-            try {
-                const response = await fetch('/api/settings', {
-                    method: 'PATCH',
-                    headers: { 'Content-Type': 'application/json' },
-                    body: JSON.stringify({ patterns: { clear_pattern_speed: speed } })
-                });
-
-                if (response.ok) {
-                    await response.json();
-                    if (speed === null) {
-                        showStatusMessage('Clear pattern speed set to default', 'success');
-                    } else {
-                        showStatusMessage(`Clear pattern speed set to ${speed} steps/min`, 'success');
-                    }
-                    
-                    // Update the effective speed display
-                    const effectiveClearSpeed = document.getElementById('effectiveClearSpeed');
-                    if (effectiveClearSpeed) {
-                        if (speed === null) {
-                            effectiveClearSpeed.textContent = `Using default pattern speed: ${data.effective_speed} steps/min`;
-                        } else {
-                            effectiveClearSpeed.textContent = `Current: ${speed} steps/min`;
-                        }
-                    }
-                } else {
-                    const error = await response.json();
-                    throw new Error(error.detail || 'Failed to save clear pattern speed');
-                }
-            } catch (error) {
-                showStatusMessage(`Failed to save clear pattern speed: ${error.message}`, 'error');
-            }
-        });
-    }
-}
-
-// Button click handlers
-document.addEventListener('DOMContentLoaded', function() {
-    // Home button
-    const homeButton = document.getElementById('homeButton');
-    if (homeButton) {
-        homeButton.addEventListener('click', async () => {
-        try {
-            const response = await fetch('/send_home', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json'
-                }
-            });
-            const data = await response.json();
-            if (data.success) {
-                updateStatus('Moving to home position...');
-            }
-        } catch (error) {
-            console.error('Error sending home command:', error);
-            updateStatus('Error: Failed to move to home position');
-        }
-        });
-    }
-
-    // Stop button
-    const stopButton = document.getElementById('stopButton');
-    if (stopButton) {
-        stopButton.addEventListener('click', async () => {
-        try {
-            const response = await fetch('/stop_execution', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json'
-                }
-            });
-            const data = await response.json();
-            if (data.success) {
-                updateStatus('Execution stopped');
-            }
-        } catch (error) {
-            console.error('Error stopping execution:', error);
-            updateStatus('Error: Failed to stop execution');
-        }
-        });
-    }
-
-    // Move to Center button
-    const centerButton = document.getElementById('centerButton');
-    if (centerButton) {
-        centerButton.addEventListener('click', async () => {
-        try {
-            const response = await fetch('/move_to_center', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json'
-                }
-            });
-            const data = await response.json();
-            if (data.success) {
-                updateStatus('Moving to center position...');
-            }
-        } catch (error) {
-            console.error('Error moving to center:', error);
-            updateStatus('Error: Failed to move to center');
-        }
-        });
-    }
-
-    // Move to Perimeter button
-    const perimeterButton = document.getElementById('perimeterButton');
-    if (perimeterButton) {
-        perimeterButton.addEventListener('click', async () => {
-        try {
-            const response = await fetch('/move_to_perimeter', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json'
-                }
-            });
-            const data = await response.json();
-            if (data.success) {
-                updateStatus('Moving to perimeter position...');
-            }
-        } catch (error) {
-            console.error('Error moving to perimeter:', error);
-            updateStatus('Error: Failed to move to perimeter');
-        }
-        });
-    }
-});
-
-// Function to update status
-function updateStatus(message) {
-    const statusElement = document.querySelector('.text-slate-800.text-base.font-medium.leading-normal');
-    if (statusElement) {
-        statusElement.textContent = message;
-        // Reset status after 3 seconds if it's a temporary message
-        if (message.includes('Moving') || message.includes('Execution')) {
-            setTimeout(() => {
-                statusElement.textContent = 'Status';
-            }, 3000);
-        }
-    }
-}
-
-// Function to show status messages (using existing base.js showStatusMessage if available)
-function showStatusMessage(message, type) {
-    if (typeof window.showStatusMessage === 'function') {
-        window.showStatusMessage(message, type);
-    } else {
-        // Fallback to console logging
-        console.log(`[${type}] ${message}`);
-    }
-}
-
-// Function to show update instructions modal
-function showUpdateInstructionsModal(data) {
-    // Create modal HTML
-    const modal = document.createElement('div');
-    modal.id = 'updateInstructionsModal';
-    modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4';
-    modal.innerHTML = `
-        <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md">
-            <div class="p-6">
-                <div class="text-center mb-4">
-                    <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-2">Manual Update Required</h2>
-                    <p class="text-gray-600 dark:text-gray-400 text-sm">
-                        ${data.message}
-                    </p>
-                </div>
-                
-                <div class="text-gray-700 dark:text-gray-300 text-sm mb-6">
-                    <p class="mb-3">${data.instructions}</p>
-                </div>
-                
-                <div class="flex gap-3 justify-center">
-                    <button id="openGitHubRelease" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
-                        View Update Instructions
-                    </button>
-                    <button id="closeUpdateModal" class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors">
-                        Close
-                    </button>
-                </div>
-            </div>
-        </div>
-    `;
-    
-    document.body.appendChild(modal);
-    
-    // Add event listeners
-    const openGitHubButton = modal.querySelector('#openGitHubRelease');
-    const closeButton = modal.querySelector('#closeUpdateModal');
-    
-    openGitHubButton.addEventListener('click', () => {
-        window.open(data.manual_update_url, '_blank');
-        document.body.removeChild(modal);
-    });
-    
-    closeButton.addEventListener('click', () => {
-        document.body.removeChild(modal);
-    });
-    
-    // Close on outside click
-    modal.addEventListener('click', (e) => {
-        if (e.target === modal) {
-            document.body.removeChild(modal);
-        }
-    });
-}
-
-// Autocomplete functionality
-function initializeAutocomplete(inputId, suggestionsId, clearButtonId, patterns) {
-    const input = document.getElementById(inputId);
-    const suggestionsDiv = document.getElementById(suggestionsId);
-    const clearButton = document.getElementById(clearButtonId);
-    let selectedIndex = -1;
-    
-    if (!input || !suggestionsDiv) return;
-    
-    // Function to update clear button visibility
-    function updateClearButton() {
-        if (clearButton) {
-            if (input.value.trim()) {
-                clearButton.classList.remove('hidden');
-            } else {
-                clearButton.classList.add('hidden');
-            }
-        }
-    }
-    
-    // Format pattern name for display
-    function formatPatternName(pattern) {
-        return pattern.replace('.thr', '').replace(/_/g, ' ');
-    }
-    
-    // Filter patterns based on input
-    function filterPatterns(searchTerm) {
-        if (!searchTerm) return patterns.slice(0, 20); // Show first 20 when empty
-        
-        const term = searchTerm.toLowerCase();
-        return patterns.filter(pattern => {
-            const name = pattern.toLowerCase();
-            return name.includes(term);
-        }).sort((a, b) => {
-            // Prioritize patterns that start with the search term
-            const aStarts = a.toLowerCase().startsWith(term);
-            const bStarts = b.toLowerCase().startsWith(term);
-            if (aStarts && !bStarts) return -1;
-            if (!aStarts && bStarts) return 1;
-            return a.localeCompare(b);
-        }).slice(0, 20); // Limit to 20 results
-    }
-    
-    // Highlight matching text
-    function highlightMatch(text, searchTerm) {
-        if (!searchTerm) return text;
-        
-        const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
-        return text.replace(regex, '<mark>$1</mark>');
-    }
-    
-    // Show suggestions
-    function showSuggestions(searchTerm) {
-        const filtered = filterPatterns(searchTerm);
-        
-        if (filtered.length === 0 && searchTerm) {
-            suggestionsDiv.innerHTML = '<div class="suggestion-item" style="cursor: default; color: #9ca3af;">No patterns found</div>';
-            suggestionsDiv.classList.remove('hidden');
-            return;
-        }
-        
-        suggestionsDiv.innerHTML = filtered.map((pattern, index) => {
-            const displayName = formatPatternName(pattern);
-            const highlighted = highlightMatch(displayName, searchTerm);
-            return `<div class="suggestion-item" data-value="${pattern}" data-index="${index}">${highlighted}</div>`;
-        }).join('');
-        
-        suggestionsDiv.classList.remove('hidden');
-        selectedIndex = -1;
-    }
-    
-    // Hide suggestions
-    function hideSuggestions() {
-        setTimeout(() => {
-            suggestionsDiv.classList.add('hidden');
-            selectedIndex = -1;
-        }, 200);
-    }
-    
-    // Select suggestion
-    function selectSuggestion(value) {
-        input.value = value;
-        hideSuggestions();
-        updateClearButton();
-    }
-    
-    // Handle keyboard navigation
-    function handleKeyboard(e) {
-        const items = suggestionsDiv.querySelectorAll('.suggestion-item[data-value]');
-        
-        if (e.key === 'ArrowDown') {
-            e.preventDefault();
-            selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
-            updateSelection(items);
-        } else if (e.key === 'ArrowUp') {
-            e.preventDefault();
-            selectedIndex = Math.max(selectedIndex - 1, -1);
-            updateSelection(items);
-        } else if (e.key === 'Enter') {
-            e.preventDefault();
-            if (selectedIndex >= 0 && items[selectedIndex]) {
-                selectSuggestion(items[selectedIndex].dataset.value);
-            } else if (items.length === 1) {
-                selectSuggestion(items[0].dataset.value);
-            }
-        } else if (e.key === 'Escape') {
-            hideSuggestions();
-        }
-    }
-    
-    // Update visual selection
-    function updateSelection(items) {
-        items.forEach((item, index) => {
-            if (index === selectedIndex) {
-                item.classList.add('selected');
-                item.scrollIntoView({ block: 'nearest' });
-            } else {
-                item.classList.remove('selected');
-            }
-        });
-    }
-    
-    // Event listeners
-    input.addEventListener('input', (e) => {
-        const value = e.target.value.trim();
-        updateClearButton();
-        if (value.length > 0 || e.target === document.activeElement) {
-            showSuggestions(value);
-        } else {
-            hideSuggestions();
-        }
-    });
-    
-    input.addEventListener('focus', () => {
-        const value = input.value.trim();
-        showSuggestions(value);
-    });
-    
-    input.addEventListener('blur', hideSuggestions);
-    
-    input.addEventListener('keydown', handleKeyboard);
-    
-    // Click handler for suggestions
-    suggestionsDiv.addEventListener('click', (e) => {
-        const item = e.target.closest('.suggestion-item[data-value]');
-        if (item) {
-            selectSuggestion(item.dataset.value);
-        }
-    });
-    
-    // Mouse hover handler
-    suggestionsDiv.addEventListener('mouseover', (e) => {
-        const item = e.target.closest('.suggestion-item[data-value]');
-        if (item) {
-            selectedIndex = parseInt(item.dataset.index);
-            const items = suggestionsDiv.querySelectorAll('.suggestion-item[data-value]');
-            updateSelection(items);
-        }
-    });
-    
-    // Clear button handler
-    if (clearButton) {
-        clearButton.addEventListener('click', () => {
-            input.value = '';
-            updateClearButton();
-            hideSuggestions();
-            input.focus();
-        });
-    }
-    
-    // Initialize clear button visibility
-    updateClearButton();
-} 
-
-// auto_play Mode Functions
-async function initializeauto_playMode() {
-    const auto_playToggle = document.getElementById('auto_playModeToggle');
-    const auto_playSettings = document.getElementById('auto_playSettings');
-    const auto_playPlaylistSelect = document.getElementById('auto_playPlaylistSelect');
-    const auto_playRunModeSelect = document.getElementById('auto_playRunModeSelect');
-    const auto_playPauseTimeInput = document.getElementById('auto_playPauseTimeInput');
-    const auto_playClearPatternSelect = document.getElementById('auto_playClearPatternSelect');
-    const auto_playShuffleToggle = document.getElementById('auto_playShuffleToggle');
-    
-    // Load current auto_play settings
-    try {
-        const response = await fetch('/api/auto_play-mode');
-        const data = await response.json();
-        
-        auto_playToggle.checked = data.enabled;
-        if (data.enabled) {
-            auto_playSettings.style.display = 'block';
-        }
-        
-        // Set current values
-        auto_playRunModeSelect.value = data.run_mode || 'loop';
-        auto_playPauseTimeInput.value = data.pause_time || 5.0;
-        auto_playClearPatternSelect.value = data.clear_pattern || 'adaptive';
-        auto_playShuffleToggle.checked = data.shuffle || false;
-        
-        // Load playlists for selection
-        const playlistsResponse = await fetch('/list_all_playlists');
-        const playlists = await playlistsResponse.json();
-        
-        // Clear and populate playlist select
-        auto_playPlaylistSelect.innerHTML = '<option value="">Select a playlist...</option>';
-        playlists.forEach(playlist => {
-            const option = document.createElement('option');
-            option.value = playlist;
-            option.textContent = playlist;
-            if (playlist === data.playlist) {
-                option.selected = true;
-            }
-            auto_playPlaylistSelect.appendChild(option);
-        });
-    } catch (error) {
-        logMessage(`Error loading auto_play settings: ${error.message}`, LOG_TYPE.ERROR);
-    }
-    
-    // Get save button
-    const saveAutoPlayButton = document.getElementById('saveAutoPlaySettings');
-
-    // Function to save settings
-    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 {
-            const response = await fetch('/api/auto_play-mode', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({
-                    enabled: auto_playToggle.checked,
-                    playlist: auto_playPlaylistSelect.value || null,
-                    run_mode: auto_playRunModeSelect.value,
-                    pause_time: parseFloat(auto_playPauseTimeInput.value) || 0,
-                    clear_pattern: auto_playClearPatternSelect.value,
-                    shuffle: auto_playShuffleToggle.checked
-                })
-            });
-
-            if (!response.ok) {
-                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) {
-            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
-    auto_playToggle.addEventListener('change', async () => {
-        auto_playSettings.style.display = auto_playToggle.checked ? 'block' : 'none';
-        await saveSettings(false); // Auto-save toggle state without full feedback
-        const statusText = auto_playToggle.checked ? 'enabled' : 'disabled';
-        showStatusMessage(`Auto-play ${statusText}`, 'success');
-    });
-
-    // Save button click handler
-    if (saveAutoPlayButton) {
-        saveAutoPlayButton.addEventListener('click', () => saveSettings(true));
-    }
-}
-
-// Initialize auto_play mode when DOM is ready
-document.addEventListener('DOMContentLoaded', function() {
-    initializeauto_playMode();
-    initializeStillSandsMode();
-    initializeHomingConfig();
-});
-
-// Still Sands Mode Functions
-async function initializeStillSandsMode() {
-    logMessage('Initializing Still Sands mode', LOG_TYPE.INFO);
-
-    const stillSandsToggle = document.getElementById('scheduledPauseToggle');
-    const stillSandsSettings = document.getElementById('scheduledPauseSettings');
-    const addTimeSlotButton = document.getElementById('addTimeSlotButton');
-    const saveStillSandsButton = document.getElementById('savePauseSettings');
-    const timeSlotsContainer = document.getElementById('timeSlotsContainer');
-    const wledControlToggle = document.getElementById('stillSandsWledControl');
-    const finishPatternToggle = document.getElementById('stillSandsFinishPattern');
-    const timezoneSelect = document.getElementById('stillSandsTimezone');
-
-    // Check if elements exist
-    if (!stillSandsToggle || !stillSandsSettings || !addTimeSlotButton || !saveStillSandsButton || !timeSlotsContainer) {
-        logMessage('Still Sands elements not found, skipping initialization', LOG_TYPE.WARNING);
-        logMessage(`Found elements: toggle=${!!stillSandsToggle}, settings=${!!stillSandsSettings}, addBtn=${!!addTimeSlotButton}, saveBtn=${!!saveStillSandsButton}, container=${!!timeSlotsContainer}`, LOG_TYPE.WARNING);
-        return;
-    }
-
-    logMessage('All Still Sands elements found successfully', LOG_TYPE.INFO);
-
-    // Track time slots
-    let timeSlots = [];
-    let slotIdCounter = 0;
-
-    // Load current Still Sands settings from initial data
-    try {
-        // Use the data loaded during page initialization, fallback to API if not available
-        let data;
-        if (window.initialStillSandsData) {
-            data = window.initialStillSandsData;
-            // Clear the global variable after use
-            delete window.initialStillSandsData;
-        } else {
-            // Fallback to API call if initial data not available
-            const response = await fetch('/api/scheduled-pause');
-            data = await response.json();
-        }
-
-        stillSandsToggle.checked = data.enabled || false;
-        if (data.enabled) {
-            stillSandsSettings.style.display = 'block';
-        }
-
-        // Load WLED control setting
-        if (wledControlToggle) {
-            wledControlToggle.checked = data.control_wled || false;
-        }
-
-        // Load finish pattern setting
-        if (finishPatternToggle) {
-            finishPatternToggle.checked = data.finish_pattern || false;
-        }
-
-        // Load timezone setting
-        if (timezoneSelect) {
-            timezoneSelect.value = data.timezone || '';
-        }
-
-        // Load existing time slots
-        timeSlots = data.time_slots || [];
-
-        // Assign IDs to loaded slots BEFORE rendering
-        if (timeSlots.length > 0) {
-            slotIdCounter = 0;
-            timeSlots.forEach(slot => {
-                slot.id = ++slotIdCounter;
-            });
-        }
-
-        renderTimeSlots();
-    } catch (error) {
-        logMessage(`Error loading Still Sands settings: ${error.message}`, LOG_TYPE.ERROR);
-        // Initialize with empty settings if load fails
-        timeSlots = [];
-        renderTimeSlots();
-    }
-
-    // Function to validate time format (HH:MM)
-    function isValidTime(timeString) {
-        const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
-        return timeRegex.test(timeString);
-    }
-
-    // Function to create a new time slot element
-    function createTimeSlotElement(slot) {
-        const slotDiv = document.createElement('div');
-        slotDiv.className = 'time-slot-item';
-        slotDiv.dataset.slotId = slot.id;
-
-        slotDiv.innerHTML = `
-            <div class="flex items-center gap-3">
-                <div class="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3">
-                    <div class="flex flex-col gap-1">
-                        <label class="text-slate-700 dark:text-slate-300 text-xs font-medium">Start Time</label>
-                        <input
-                            type="time"
-                            class="start-time form-input 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-9 px-3 text-sm font-normal leading-normal transition-colors"
-                            value="${slot.start_time || ''}"
-                            required
-                        />
-                    </div>
-                    <div class="flex flex-col gap-1">
-                        <label class="text-slate-700 dark:text-slate-300 text-xs font-medium">End Time</label>
-                        <input
-                            type="time"
-                            class="end-time form-input 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-9 px-3 text-sm font-normal leading-normal transition-colors"
-                            value="${slot.end_time || ''}"
-                            required
-                        />
-                    </div>
-                </div>
-                <div class="flex flex-col gap-1">
-                    <label class="text-slate-700 dark:text-slate-300 text-xs font-medium">Days</label>
-                    <select class="days-select form-select 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-9 px-3 text-sm font-normal transition-colors">
-                        <option value="daily" ${slot.days === 'daily' ? 'selected' : ''}>Daily</option>
-                        <option value="weekdays" ${slot.days === 'weekdays' ? 'selected' : ''}>Weekdays</option>
-                        <option value="weekends" ${slot.days === 'weekends' ? 'selected' : ''}>Weekends</option>
-                        <option value="custom" ${slot.days === 'custom' ? 'selected' : ''}>Custom</option>
-                    </select>
-                </div>
-                <button
-                    type="button"
-                    class="remove-slot-btn flex items-center justify-center w-9 h-9 text-red-600 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-900/20 rounded-lg transition-colors"
-                    title="Remove time slot"
-                >
-                    <span class="material-icons text-base">delete</span>
-                </button>
-            </div>
-            <div class="custom-days-container mt-2" style="display: ${slot.days === 'custom' ? 'block' : 'none'};">
-                <label class="text-slate-700 dark:text-slate-300 text-xs font-medium mb-1 block">Select Days</label>
-                <div class="flex flex-wrap gap-2">
-                    ${['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'].map(day => `
-                        <label class="flex items-center gap-1 text-xs">
-                            <input
-                                type="checkbox"
-                                name="custom-days-${slot.id}"
-                                value="${day}"
-                                ${slot.custom_days && slot.custom_days.includes(day) ? 'checked' : ''}
-                                class="rounded border-slate-300 text-sky-600 focus:ring-sky-500"
-                            />
-                            <span class="text-slate-700 dark:text-slate-300 capitalize">${day.substring(0, 3)}</span>
-                        </label>
-                    `).join('')}
-                </div>
-            </div>
-        `;
-
-        // Add event listeners for this slot
-        const startTimeInput = slotDiv.querySelector('.start-time');
-        const endTimeInput = slotDiv.querySelector('.end-time');
-        const daysSelect = slotDiv.querySelector('.days-select');
-        const customDaysContainer = slotDiv.querySelector('.custom-days-container');
-        const removeButton = slotDiv.querySelector('.remove-slot-btn');
-
-        // Show/hide custom days based on selection
-        daysSelect.addEventListener('change', () => {
-            customDaysContainer.style.display = daysSelect.value === 'custom' ? 'block' : 'none';
-            updateTimeSlot(slot.id);
-        });
-
-        // Update slot data when inputs change
-        startTimeInput.addEventListener('change', () => updateTimeSlot(slot.id));
-        endTimeInput.addEventListener('change', () => updateTimeSlot(slot.id));
-
-        // Handle custom day checkboxes
-        customDaysContainer.addEventListener('change', () => updateTimeSlot(slot.id));
-
-        // Remove slot button
-        removeButton.addEventListener('click', () => {
-            removeTimeSlot(slot.id);
-        });
-
-        return slotDiv;
-    }
-
-    // Function to render all time slots
-    function renderTimeSlots() {
-        timeSlotsContainer.innerHTML = '';
-
-        if (timeSlots.length === 0) {
-            timeSlotsContainer.innerHTML = `
-                <div class="text-center py-8 text-slate-500 dark:text-slate-400">
-                    <span class="material-icons text-4xl mb-2 block">schedule</span>
-                    <p>No time slots configured</p>
-                    <p class="text-xs mt-1">Click "Add Time Slot" to create a pause schedule</p>
-                </div>
-            `;
-            return;
-        }
-
-        timeSlots.forEach(slot => {
-            const slotElement = createTimeSlotElement(slot);
-            timeSlotsContainer.appendChild(slotElement);
-        });
-    }
-
-    // Function to add a new time slot
-    function addTimeSlot() {
-        const newSlot = {
-            id: ++slotIdCounter,
-            start_time: '22:00',
-            end_time: '08:00',
-            days: 'daily',
-            custom_days: []
-        };
-
-        timeSlots.push(newSlot);
-        renderTimeSlots();
-    }
-
-    // Function to remove a time slot
-    function removeTimeSlot(slotId) {
-        timeSlots = timeSlots.filter(slot => slot.id !== slotId);
-        renderTimeSlots();
-    }
-
-    // Function to update a time slot's data
-    function updateTimeSlot(slotId) {
-        const slotElement = timeSlotsContainer.querySelector(`[data-slot-id="${slotId}"]`);
-        if (!slotElement) return;
-
-        const slot = timeSlots.find(s => s.id === slotId);
-        if (!slot) return;
-
-        // Update slot data from inputs
-        slot.start_time = slotElement.querySelector('.start-time').value;
-        slot.end_time = slotElement.querySelector('.end-time').value;
-        slot.days = slotElement.querySelector('.days-select').value;
-
-        // Update custom days if applicable
-        if (slot.days === 'custom') {
-            const checkedDays = Array.from(slotElement.querySelectorAll(`input[name="custom-days-${slotId}"]:checked`))
-                .map(cb => cb.value);
-            slot.custom_days = checkedDays;
-        } else {
-            slot.custom_days = [];
-        }
-    }
-
-    // Function to validate all time slots
-    function validateTimeSlots() {
-        const errors = [];
-
-        timeSlots.forEach((slot, index) => {
-            if (!slot.start_time || !isValidTime(slot.start_time)) {
-                errors.push(`Time slot ${index + 1}: Invalid start time`);
-            }
-            if (!slot.end_time || !isValidTime(slot.end_time)) {
-                errors.push(`Time slot ${index + 1}: Invalid end time`);
-            }
-            if (slot.days === 'custom' && (!slot.custom_days || slot.custom_days.length === 0)) {
-                errors.push(`Time slot ${index + 1}: Please select at least one day for custom schedule`);
-            }
-        });
-
-        return errors;
-    }
-
-    // Function to save settings
-    async function saveStillSandsSettings() {
-        // Update all slots from current form values
-        timeSlots.forEach(slot => updateTimeSlot(slot.id));
-
-        // Validate time slots
-        const validationErrors = validateTimeSlots();
-        if (validationErrors.length > 0) {
-            showStatusMessage(`Validation errors: ${validationErrors.join(', ')}`, 'error');
-            return;
-        }
-
-        // Update button UI to show loading state
-        const originalButtonHTML = saveStillSandsButton.innerHTML;
-        saveStillSandsButton.disabled = true;
-        saveStillSandsButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Saving...</span>';
-
-        try {
-            const response = await fetch('/api/scheduled-pause', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify({
-                    enabled: stillSandsToggle.checked,
-                    control_wled: wledControlToggle ? wledControlToggle.checked : false,
-                    finish_pattern: finishPatternToggle ? finishPatternToggle.checked : false,
-                    timezone: timezoneSelect ? (timezoneSelect.value || null) : null,
-                    time_slots: timeSlots.map(slot => ({
-                        start_time: slot.start_time,
-                        end_time: slot.end_time,
-                        days: slot.days,
-                        custom_days: slot.custom_days
-                    }))
-                })
-            });
-
-            if (!response.ok) {
-                const errorData = await response.json();
-                throw new Error(errorData.detail || 'Failed to save Still Sands settings');
-            }
-
-            // Show success state temporarily
-            saveStillSandsButton.innerHTML = '<span class="material-icons text-lg">check</span><span class="truncate">Saved!</span>';
-            showStatusMessage('Still Sands settings saved successfully', 'success');
-
-            // Restore button after 2 seconds
-            setTimeout(() => {
-                saveStillSandsButton.innerHTML = originalButtonHTML;
-                saveStillSandsButton.disabled = false;
-            }, 2000);
-        } catch (error) {
-            logMessage(`Error saving Still Sands settings: ${error.message}`, LOG_TYPE.ERROR);
-            showStatusMessage(`Failed to save settings: ${error.message}`, 'error');
-
-            // Restore button immediately on error
-            saveStillSandsButton.innerHTML = originalButtonHTML;
-            saveStillSandsButton.disabled = false;
-        }
-    }
-
-    // Note: Slot IDs are now assigned during initialization above, before first render
-
-    // Event listeners
-    stillSandsToggle.addEventListener('change', async () => {
-        logMessage(`Still Sands toggle changed: ${stillSandsToggle.checked}`, LOG_TYPE.INFO);
-        stillSandsSettings.style.display = stillSandsToggle.checked ? 'block' : 'none';
-        logMessage(`Settings display set to: ${stillSandsSettings.style.display}`, LOG_TYPE.INFO);
-
-        // Auto-save when toggle changes
-        try {
-            await saveStillSandsSettings();
-            const statusText = stillSandsToggle.checked ? 'enabled' : 'disabled';
-            showStatusMessage(`Still Sands ${statusText} successfully`, 'success');
-        } catch (error) {
-            logMessage(`Error saving Still Sands toggle: ${error.message}`, LOG_TYPE.ERROR);
-            showStatusMessage(`Failed to save Still Sands setting: ${error.message}`, 'error');
-        }
-    });
-
-    addTimeSlotButton.addEventListener('click', addTimeSlot);
-    saveStillSandsButton.addEventListener('click', saveStillSandsSettings);
-
-    // Add listener for WLED control toggle
-    if (wledControlToggle) {
-        wledControlToggle.addEventListener('change', async () => {
-            logMessage(`WLED control toggle changed: ${wledControlToggle.checked}`, LOG_TYPE.INFO);
-            // Auto-save when WLED control changes
-            await saveStillSandsSettings();
-        });
-    }
-
-    // Add listener for finish pattern toggle
-    if (finishPatternToggle) {
-        finishPatternToggle.addEventListener('change', async () => {
-            logMessage(`Finish pattern toggle changed: ${finishPatternToggle.checked}`, LOG_TYPE.INFO);
-            // Auto-save when finish pattern setting changes
-            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
-async function initializeHomingConfig() {
-    logMessage('Initializing homing configuration', LOG_TYPE.INFO);
-
-    const homingModeCrash = document.getElementById('homingModeCrash');
-    const homingModeSensor = document.getElementById('homingModeSensor');
-    const angularOffsetInput = document.getElementById('angularOffsetInput');
-    const compassOffsetContainer = document.getElementById('compassOffsetContainer');
-    const saveHomingConfigButton = document.getElementById('saveHomingConfig');
-    const homingInfoContent = document.getElementById('homingInfoContent');
-    const autoHomeEnabledToggle = document.getElementById('autoHomeEnabledToggle');
-    const autoHomeSettings = document.getElementById('autoHomeSettings');
-    const autoHomeAfterPatternsInput = document.getElementById('autoHomeAfterPatternsInput');
-
-    // Check if elements exist
-    if (!homingModeCrash || !homingModeSensor || !angularOffsetInput || !saveHomingConfigButton || !homingInfoContent || !compassOffsetContainer) {
-        logMessage('Homing configuration elements not found, skipping initialization', LOG_TYPE.WARNING);
-        return;
-    }
-
-    logMessage('Homing configuration elements found successfully', LOG_TYPE.INFO);
-
-    // Function to get selected homing mode
-    function getSelectedMode() {
-        return homingModeCrash.checked ? 0 : 1;
-    }
-
-    // Function to update info box and visibility based on selected mode
-    function updateHomingInfo() {
-        const mode = getSelectedMode();
-
-        // Show/hide compass offset based on mode
-        if (mode === 0) {
-            compassOffsetContainer.style.display = 'none';
-            homingInfoContent.innerHTML = `
-                <p class="font-medium text-blue-800">Crash Homing Mode:</p>
-                <ul class="mt-1 space-y-1 text-blue-700">
-                    <li>• Y axis moves -22mm (or -30mm for mini) until physical stop</li>
-                    <li>• Theta set to 0, rho set to 0</li>
-                    <li>• No x0 y0 command sent</li>
-                    <li>• No hardware sensors required</li>
-                </ul>
-            `;
-        } else {
-            compassOffsetContainer.style.display = 'block';
-            homingInfoContent.innerHTML = `
-                <p class="font-medium text-blue-800">Sensor Homing Mode:</p>
-                <ul class="mt-1 space-y-1 text-blue-700">
-                    <li>• Requires hardware limit switches</li>
-                    <li>• Requires additional configuration</li>
-                </ul>
-            `;
-        }
-    }
-
-    // Load current homing configuration
-    try {
-        const response = await fetch('/api/homing-config');
-        const data = await response.json();
-
-        // Set radio button based on mode
-        if (data.homing_mode === 1) {
-            homingModeSensor.checked = true;
-        } else {
-            homingModeCrash.checked = true;
-        }
-
-        angularOffsetInput.value = data.angular_homing_offset_degrees || 0;
-
-        // Load auto-home settings
-        if (autoHomeEnabledToggle) {
-            autoHomeEnabledToggle.checked = data.auto_home_enabled || false;
-            if (autoHomeSettings) {
-                autoHomeSettings.style.display = data.auto_home_enabled ? 'block' : 'none';
-            }
-        }
-        if (autoHomeAfterPatternsInput) {
-            autoHomeAfterPatternsInput.value = data.auto_home_after_patterns || 5;
-        }
-
-        updateHomingInfo();
-
-        logMessage(`Loaded homing config: mode=${data.homing_mode}, offset=${data.angular_homing_offset_degrees}°, auto_home=${data.auto_home_enabled}, after=${data.auto_home_after_patterns}`, LOG_TYPE.INFO);
-    } catch (error) {
-        logMessage(`Error loading homing configuration: ${error.message}`, LOG_TYPE.ERROR);
-        // Initialize with defaults if load fails
-        homingModeCrash.checked = true;
-        angularOffsetInput.value = 0;
-        if (autoHomeEnabledToggle) autoHomeEnabledToggle.checked = false;
-        if (autoHomeAfterPatternsInput) autoHomeAfterPatternsInput.value = 5;
-        updateHomingInfo();
-    }
-
-    // Function to save homing configuration
-    async function saveHomingConfig() {
-        // Update button UI to show loading state
-        const originalButtonHTML = saveHomingConfigButton.innerHTML;
-        saveHomingConfigButton.disabled = true;
-        saveHomingConfigButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Saving...</span>';
-
-        try {
-            const requestBody = {
-                homing_mode: getSelectedMode(),
-                angular_homing_offset_degrees: parseFloat(angularOffsetInput.value) || 0
-            };
-
-            // Include auto-home settings if elements exist
-            if (autoHomeEnabledToggle) {
-                requestBody.auto_home_enabled = autoHomeEnabledToggle.checked;
-            }
-            if (autoHomeAfterPatternsInput) {
-                const afterPatterns = parseInt(autoHomeAfterPatternsInput.value);
-                if (!isNaN(afterPatterns) && afterPatterns >= 1) {
-                    requestBody.auto_home_after_patterns = afterPatterns;
-                }
-            }
-
-            const response = await fetch('/api/homing-config', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify(requestBody)
-            });
-
-            if (!response.ok) {
-                const errorData = await response.json();
-                throw new Error(errorData.detail || 'Failed to save homing configuration');
-            }
-
-            // Show success state temporarily
-            saveHomingConfigButton.innerHTML = '<span class="material-icons text-lg">check</span><span class="truncate">Saved!</span>';
-            showStatusMessage('Homing configuration saved successfully', 'success');
-
-            // Restore button after 2 seconds
-            setTimeout(() => {
-                saveHomingConfigButton.innerHTML = originalButtonHTML;
-                saveHomingConfigButton.disabled = false;
-            }, 2000);
-        } catch (error) {
-            logMessage(`Error saving homing configuration: ${error.message}`, LOG_TYPE.ERROR);
-            showStatusMessage(`Failed to save homing configuration: ${error.message}`, 'error');
-
-            // Restore button immediately on error
-            saveHomingConfigButton.innerHTML = originalButtonHTML;
-            saveHomingConfigButton.disabled = false;
-        }
-    }
-
-    // Event listeners
-    homingModeCrash.addEventListener('change', updateHomingInfo);
-    homingModeSensor.addEventListener('change', updateHomingInfo);
-    saveHomingConfigButton.addEventListener('click', saveHomingConfig);
-
-    // Auto-home toggle event listener
-    if (autoHomeEnabledToggle && autoHomeSettings) {
-        autoHomeEnabledToggle.addEventListener('change', () => {
-            autoHomeSettings.style.display = autoHomeEnabledToggle.checked ? 'block' : 'none';
-        });
-    }
-}
-
-// Toggle password visibility helper
-function togglePasswordVisibility(inputId, button) {
-    const input = document.getElementById(inputId);
-    if (!input || !button) return;
-
-    const icon = button.querySelector('.material-icons');
-    if (input.type === 'password') {
-        input.type = 'text';
-        if (icon) icon.textContent = 'visibility';
-    } else {
-        input.type = 'password';
-        if (icon) icon.textContent = 'visibility_off';
-    }
-}
-
-// MQTT Configuration
-async function initializeMqttConfig() {
-    logMessage('Initializing MQTT configuration', LOG_TYPE.INFO);
-
-    const mqttEnableToggle = document.getElementById('mqttEnableToggle');
-    const mqttSettings = document.getElementById('mqttSettings');
-    const mqttStatusBanner = document.getElementById('mqttStatusBanner');
-    const mqttConnectedBanner = document.getElementById('mqttConnectedBanner');
-    const mqttDisconnectedBanner = document.getElementById('mqttDisconnectedBanner');
-    const mqttBrokerInput = document.getElementById('mqttBrokerInput');
-    const mqttPortInput = document.getElementById('mqttPortInput');
-    const mqttUsernameInput = document.getElementById('mqttUsernameInput');
-    const mqttPasswordInput = document.getElementById('mqttPasswordInput');
-    const mqttDeviceNameInput = document.getElementById('mqttDeviceNameInput');
-    const mqttDeviceIdInput = document.getElementById('mqttDeviceIdInput');
-    const mqttClientIdInput = document.getElementById('mqttClientIdInput');
-    const mqttDiscoveryPrefixInput = document.getElementById('mqttDiscoveryPrefixInput');
-    const testMqttButton = document.getElementById('testMqttConnection');
-    const mqttTestResult = document.getElementById('mqttTestResult');
-    const saveMqttButton = document.getElementById('saveMqttConfig');
-    const mqttRestartNotice = document.getElementById('mqttRestartNotice');
-
-    // Check if elements exist
-    if (!mqttEnableToggle || !mqttSettings || !saveMqttButton) {
-        logMessage('MQTT configuration elements not found, skipping initialization', LOG_TYPE.WARNING);
-        return;
-    }
-
-    logMessage('MQTT configuration elements found successfully', LOG_TYPE.INFO);
-
-    // Track if settings have changed (to show restart notice)
-    let originalConfig = null;
-    let configChanged = false;
-
-    // Function to update UI based on enabled state
-    function updateMqttSettingsVisibility() {
-        mqttSettings.style.display = mqttEnableToggle.checked ? 'block' : 'none';
-        if (mqttStatusBanner) {
-            mqttStatusBanner.classList.toggle('hidden', !mqttEnableToggle.checked);
-        }
-    }
-
-    // Function to update connection status banners
-    function updateConnectionStatus(connected) {
-        if (mqttConnectedBanner && mqttDisconnectedBanner) {
-            if (connected) {
-                mqttConnectedBanner.classList.remove('hidden');
-                mqttDisconnectedBanner.classList.add('hidden');
-            } else {
-                mqttConnectedBanner.classList.add('hidden');
-                mqttDisconnectedBanner.classList.remove('hidden');
-            }
-        }
-    }
-
-    // Function to check if config has changed
-    function checkConfigChanged() {
-        if (!originalConfig) return false;
-
-        const currentConfig = {
-            enabled: mqttEnableToggle.checked,
-            broker: mqttBrokerInput.value,
-            port: parseInt(mqttPortInput.value) || 1883,
-            username: mqttUsernameInput.value,
-            password: mqttPasswordInput.value,
-            device_name: mqttDeviceNameInput.value,
-            device_id: mqttDeviceIdInput.value,
-            client_id: mqttClientIdInput.value,
-            discovery_prefix: mqttDiscoveryPrefixInput.value
-        };
-
-        return JSON.stringify(currentConfig) !== JSON.stringify(originalConfig);
-    }
-
-    // Function to show/hide restart notice
-    function updateRestartNotice() {
-        configChanged = checkConfigChanged();
-        if (mqttRestartNotice) {
-            mqttRestartNotice.classList.toggle('hidden', !configChanged);
-        }
-    }
-
-    // Load current MQTT configuration
-    try {
-        const response = await fetch('/api/mqtt-config');
-        const data = await response.json();
-
-        mqttEnableToggle.checked = data.enabled || false;
-        mqttBrokerInput.value = data.broker || '';
-        mqttPortInput.value = data.port || 1883;
-        mqttUsernameInput.value = data.username || '';
-        // Note: Password is not returned from API for security
-        mqttDeviceNameInput.value = data.device_name || 'Dune Weaver';
-        mqttDeviceIdInput.value = data.device_id || 'dune_weaver';
-        mqttClientIdInput.value = data.client_id || 'dune_weaver';
-        mqttDiscoveryPrefixInput.value = data.discovery_prefix || 'homeassistant';
-
-        // Store original config for change detection
-        originalConfig = {
-            enabled: data.enabled || false,
-            broker: data.broker || '',
-            port: data.port || 1883,
-            username: data.username || '',
-            password: '', // We don't have the original password
-            device_name: data.device_name || 'Dune Weaver',
-            device_id: data.device_id || 'dune_weaver',
-            client_id: data.client_id || 'dune_weaver',
-            discovery_prefix: data.discovery_prefix || 'homeassistant'
-        };
-
-        updateMqttSettingsVisibility();
-
-        // Update connection status if MQTT is enabled
-        if (data.enabled) {
-            updateConnectionStatus(data.connected || false);
-        }
-
-        logMessage(`Loaded MQTT config: enabled=${data.enabled}, broker=${data.broker}`, LOG_TYPE.INFO);
-    } catch (error) {
-        logMessage(`Error loading MQTT configuration: ${error.message}`, LOG_TYPE.ERROR);
-        // Initialize with defaults if load fails
-        mqttEnableToggle.checked = false;
-        updateMqttSettingsVisibility();
-    }
-
-    // Function to save MQTT configuration
-    async function saveMqttConfig() {
-        // Validate required fields if MQTT is enabled
-        if (mqttEnableToggle.checked && !mqttBrokerInput.value.trim()) {
-            showStatusMessage('MQTT broker address is required when MQTT is enabled', 'error');
-            mqttBrokerInput.focus();
-            return;
-        }
-
-        // Update button UI to show loading state
-        const originalButtonHTML = saveMqttButton.innerHTML;
-        saveMqttButton.disabled = true;
-        saveMqttButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Saving...</span>';
-
-        try {
-            const requestBody = {
-                enabled: mqttEnableToggle.checked,
-                broker: mqttBrokerInput.value.trim(),
-                port: parseInt(mqttPortInput.value) || 1883,
-                username: mqttUsernameInput.value.trim() || null,
-                device_name: mqttDeviceNameInput.value.trim() || 'Dune Weaver',
-                device_id: mqttDeviceIdInput.value.trim() || 'dune_weaver',
-                client_id: mqttClientIdInput.value.trim() || 'dune_weaver',
-                discovery_prefix: mqttDiscoveryPrefixInput.value.trim() || 'homeassistant'
-            };
-
-            // Only include password if it was changed (not empty)
-            if (mqttPasswordInput.value) {
-                requestBody.password = mqttPasswordInput.value;
-            }
-
-            const response = await fetch('/api/mqtt-config', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify(requestBody)
-            });
-
-            if (!response.ok) {
-                const errorData = await response.json();
-                throw new Error(errorData.detail || 'Failed to save MQTT configuration');
-            }
-
-            const data = await response.json();
-
-            // Update original config for change detection
-            originalConfig = {
-                enabled: requestBody.enabled,
-                broker: requestBody.broker,
-                port: requestBody.port,
-                username: requestBody.username || '',
-                password: '', // Reset password tracking
-                device_name: requestBody.device_name,
-                device_id: requestBody.device_id,
-                client_id: requestBody.client_id,
-                discovery_prefix: requestBody.discovery_prefix
-            };
-
-            // Clear password field after save
-            mqttPasswordInput.value = '';
-
-            // Show success state temporarily
-            saveMqttButton.innerHTML = '<span class="material-icons text-lg">check</span><span class="truncate">Saved!</span>';
-            showStatusMessage('MQTT configuration saved successfully. Restart the application to apply changes.', 'success');
-
-            // Show restart notice
-            if (mqttRestartNotice) {
-                mqttRestartNotice.classList.remove('hidden');
-            }
-
-            // Restore button after 2 seconds
-            setTimeout(() => {
-                saveMqttButton.innerHTML = originalButtonHTML;
-                saveMqttButton.disabled = false;
-            }, 2000);
-        } catch (error) {
-            logMessage(`Error saving MQTT configuration: ${error.message}`, LOG_TYPE.ERROR);
-            showStatusMessage(`Failed to save MQTT configuration: ${error.message}`, 'error');
-
-            // Restore button immediately on error
-            saveMqttButton.innerHTML = originalButtonHTML;
-            saveMqttButton.disabled = false;
-        }
-    }
-
-    // Function to test MQTT connection
-    async function testMqttConnection() {
-        // Validate broker address
-        if (!mqttBrokerInput.value.trim()) {
-            showStatusMessage('Please enter a broker address to test', 'error');
-            mqttBrokerInput.focus();
-            return;
-        }
-
-        // Update button UI to show loading state
-        const originalButtonHTML = testMqttButton.innerHTML;
-        testMqttButton.disabled = true;
-        testMqttButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Testing...</span>';
-
-        // Clear previous result
-        if (mqttTestResult) {
-            mqttTestResult.innerHTML = '';
-        }
-
-        try {
-            const requestBody = {
-                broker: mqttBrokerInput.value.trim(),
-                port: parseInt(mqttPortInput.value) || 1883,
-                username: mqttUsernameInput.value.trim() || null,
-                password: mqttPasswordInput.value || null
-            };
-
-            const response = await fetch('/api/mqtt-test', {
-                method: 'POST',
-                headers: { 'Content-Type': 'application/json' },
-                body: JSON.stringify(requestBody)
-            });
-
-            const data = await response.json();
-
-            if (data.success) {
-                if (mqttTestResult) {
-                    mqttTestResult.innerHTML = '<span class="material-icons text-green-600 mr-1">check_circle</span><span class="text-green-600">Connection successful!</span>';
-                }
-                showStatusMessage('MQTT connection test successful', 'success');
-            } else {
-                if (mqttTestResult) {
-                    mqttTestResult.innerHTML = `<span class="material-icons text-red-600 mr-1">error</span><span class="text-red-600">${data.error || 'Connection failed'}</span>`;
-                }
-                showStatusMessage(`MQTT test failed: ${data.error || 'Connection failed'}`, 'error');
-            }
-        } catch (error) {
-            logMessage(`Error testing MQTT connection: ${error.message}`, LOG_TYPE.ERROR);
-            if (mqttTestResult) {
-                mqttTestResult.innerHTML = `<span class="material-icons text-red-600 mr-1">error</span><span class="text-red-600">Test failed: ${error.message}</span>`;
-            }
-            showStatusMessage(`MQTT test failed: ${error.message}`, 'error');
-        } finally {
-            // Restore button
-            testMqttButton.innerHTML = originalButtonHTML;
-            testMqttButton.disabled = false;
-        }
-    }
-
-    // Event listeners
-    mqttEnableToggle.addEventListener('change', () => {
-        updateMqttSettingsVisibility();
-        updateRestartNotice();
-    });
-
-    // Track changes to show restart notice
-    [mqttBrokerInput, mqttPortInput, mqttUsernameInput, mqttPasswordInput,
-     mqttDeviceNameInput, mqttDeviceIdInput, mqttClientIdInput, mqttDiscoveryPrefixInput].forEach(input => {
-        if (input) {
-            input.addEventListener('input', updateRestartNotice);
-        }
-    });
-
-    testMqttButton.addEventListener('click', testMqttConnection);
-    saveMqttButton.addEventListener('click', saveMqttConfig);
-}
-
-// Initialize MQTT config when DOM is ready
-document.addEventListener('DOMContentLoaded', function() {
-    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;
-        }
-    }
-}

+ 0 - 209
static/js/table_control.js

@@ -1,209 +0,0 @@
-// Button click handlers
-document.addEventListener('DOMContentLoaded', function() {
-    // Home button
-    const homeButton = document.getElementById('homeButton');
-    homeButton.addEventListener('click', async () => {
-        try {
-            const response = await fetch('/send_home', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json'
-                }
-            });
-            const data = await response.json();
-            if (data.success) {
-                updateStatus('Moving to home position...');
-            }
-        } catch (error) {
-            console.error('Error sending home command:', error);
-            updateStatus('Error: Failed to move to home position');
-        }
-    });
-
-    // Stop button
-    const stopButton = document.getElementById('stopButton');
-    stopButton.addEventListener('click', async () => {
-        try {
-            const response = await fetch('/stop_execution', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json'
-                }
-            });
-            const data = await response.json();
-            if (data.success) {
-                updateStatus('Execution stopped');
-            }
-        } catch (error) {
-            console.error('Error stopping execution:', error);
-            updateStatus('Error: Failed to stop execution');
-        }
-    });
-
-    // Move to Center button
-    const centerButton = document.getElementById('centerButton');
-    centerButton.addEventListener('click', async () => {
-        try {
-            const response = await fetch('/move_to_center', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json'
-                }
-            });
-            const data = await response.json();
-            if (data.success) {
-                updateStatus('Moving to center position...');
-            }
-        } catch (error) {
-            console.error('Error moving to center:', error);
-            updateStatus('Error: Failed to move to center');
-        }
-    });
-
-    // Move to Perimeter button
-    const perimeterButton = document.getElementById('perimeterButton');
-    perimeterButton.addEventListener('click', async () => {
-        try {
-            const response = await fetch('/move_to_perimeter', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json'
-                }
-            });
-            const data = await response.json();
-            if (data.success) {
-                updateStatus('Moving to perimeter position...');
-            }
-        } catch (error) {
-            console.error('Error moving to perimeter:', error);
-            updateStatus('Error: Failed to move to perimeter');
-        }
-    });
-
-    // Set Speed button
-    const setSpeedButton = document.getElementById('setSpeedButton');
-    const speedInput = document.getElementById('speedInput');
-    setSpeedButton.addEventListener('click', async () => {
-        const speed = parseFloat(speedInput.value);
-        if (isNaN(speed) || speed <= 0) {
-            updateStatus('Error: Please enter a valid speed value');
-            return;
-        }
-
-        try {
-            const response = await fetch('/set_speed', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json'
-                },
-                body: JSON.stringify({ speed: speed })
-            });
-            const data = await response.json();
-            if (data.success) {
-                updateStatus(`Speed set to ${speed} mm/s`);
-            }
-        } catch (error) {
-            console.error('Error setting speed:', error);
-            updateStatus('Error: Failed to set speed');
-        }
-    });
-
-    // Clear from Center button
-    const clearCenterButton = document.getElementById('clearCenterButton');
-    clearCenterButton.addEventListener('click', async () => {
-        try {
-            const response = await fetch('/run_theta_rho', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json'
-                },
-                body: JSON.stringify({
-                    file_name: 'clear_from_in.thr',
-                    pre_execution: 'none'
-                })
-            });
-            const data = await response.json();
-            if (response.ok) {
-                updateStatus('Running clear from center pattern...');
-            } else {
-                throw new Error(data.detail || 'Failed to run clear pattern');
-            }
-        } catch (error) {
-            console.error('Error running clear from center pattern:', error);
-            if (error.message.includes('409')) {
-                updateStatus('Error: Another pattern is already running');
-            } else {
-                updateStatus('Error: Failed to run clear pattern');
-            }
-        }
-    });
-
-    // Clear from Perimeter button
-    const clearPerimeterButton = document.getElementById('clearPerimeterButton');
-    clearPerimeterButton.addEventListener('click', async () => {
-        try {
-            const response = await fetch('/run_theta_rho', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json'
-                },
-                body: JSON.stringify({
-                    file_name: 'clear_from_out.thr',
-                    pre_execution: 'none'
-                })
-            });
-            const data = await response.json();
-            if (response.ok) {
-                updateStatus('Running clear from perimeter pattern...');
-            } else {
-                throw new Error(data.detail || 'Failed to run clear pattern');
-            }
-        } catch (error) {
-            console.error('Error running clear from perimeter pattern:', error);
-            if (error.message.includes('409')) {
-                updateStatus('Error: Another pattern is already running');
-            } else {
-                updateStatus('Error: Failed to run clear pattern');
-            }
-        }
-    });
-
-    // Clear Sideways button
-    const clearSidewaysButton = document.getElementById('clearSidewaysButton');
-    clearSidewaysButton.addEventListener('click', async () => {
-        try {
-            const response = await fetch('/run_theta_rho', {
-                method: 'POST',
-                headers: {
-                    'Content-Type': 'application/json'
-                },
-                body: JSON.stringify({
-                    file_name: 'clear_sideway.thr',
-                    pre_execution: 'none'
-                })
-            });
-            const data = await response.json();
-            if (response.ok) {
-                updateStatus('Running clear sideways pattern...');
-            } else {
-                throw new Error(data.detail || 'Failed to run clear pattern');
-            }
-        } catch (error) {
-            console.error('Error running clear sideways pattern:', error);
-            if (error.message.includes('409')) {
-                updateStatus('Error: Another pattern is already running');
-            } else {
-                updateStatus('Error: Failed to run clear pattern');
-            }
-        }
-    });
-});
-
-// Function to update status
-function updateStatus(message) {
-    if (message.startsWith('Error:')) {
-        showStatusMessage(message.substring(7), 'error');
-    } else {
-        showStatusMessage(message, 'success');
-    }
-} 

BIN
static/webfonts/Roboto-Italic-VariableFont_wdth,wght.ttf


BIN
static/webfonts/Roboto-VariableFont_wdth,wght.ttf


BIN
static/webfonts/fa-regular-400.ttf


BIN
static/webfonts/fa-regular-400.woff2


BIN
static/webfonts/fa-solid-900.ttf


BIN
static/webfonts/fa-solid-900.woff2


+ 0 - 19
tailwind.config.js

@@ -1,19 +0,0 @@
-/** @type {import('tailwindcss').Config} */
-module.exports = {
-  content: [
-    "./templates/**/*.html",
-    "./static/js/**/*.js",
-  ],
-  darkMode: 'class', // Enable class-based dark mode
-  theme: {
-    extend: {
-      fontFamily: {
-        sans: ['Plus Jakarta Sans', 'Noto Sans', 'sans-serif'],
-      },
-    },
-  },
-  plugins: [
-    require('@tailwindcss/forms'),
-    require('@tailwindcss/container-queries'),
-  ],
-} 

+ 0 - 1196
templates/base.html

@@ -1,1196 +0,0 @@
-<!DOCTYPE html>
-<html lang="en">
-  <head>
-    <script>
-      // Immediately set dark mode if needed, before page loads
-      const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
-      const savedTheme = localStorage.getItem('theme');
-      if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
-        document.documentElement.classList.add('dark');
-      }
-    </script>
-    <meta charset="utf-8" />
-    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <link crossorigin="" href="https://fonts.gstatic.com/" rel="preconnect" />
-    <link
-      as="style"
-      href="https://fonts.googleapis.com/css2?display=swap&amp;family=Noto+Sans%3Awght%40400%3B500%3B700%3B900&amp;family=Plus+Jakarta+Sans%3Awght%40400%3B500%3B700%3B800"
-      onload="this.rel='stylesheet'"
-      rel="stylesheet"
-    />
-    <!-- Preload Material Icons fonts for faster loading -->
-    <link rel="preload" href="/static/fonts/material-icons/MaterialIcons-Regular.woff2" as="font" type="font/woff2" crossorigin>
-    <link rel="preload" href="/static/fonts/material-icons/MaterialIconsOutlined-Regular.woff2" as="font" type="font/woff2" crossorigin>
-    <title>{% block title %}{{ app_name or 'Dune Weaver' }}{% endblock %}</title>
-    {% if custom_logo %}
-    {# Favicon is auto-generated from logo as favicon.ico #}
-    <link rel="apple-touch-icon" sizes="180x180" href="/static/custom/{{ custom_logo }}">
-    <link rel="icon" type="image/x-icon" href="/static/custom/favicon.ico">
-    {% else %}
-    <link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png">
-    <link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png">
-    <link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png">
-    <link rel="icon" type="image/x-icon" href="/static/favicon.ico">
-    {% endif %}
-    <link rel="manifest" href="/static/site.webmanifest?v=2">
-    <link rel="stylesheet" href="/static/css/tailwind.css">
-    <link rel="stylesheet" href="/static/css/material-icons.css">
-    <style>
-
-      /* Mobile navigation styles */
-      @media (max-width: 640px) {
-        nav.flex {
-          display: grid;
-          grid-template-columns: repeat(5, 1fr);
-          gap: 0;
-        }
-        nav.flex a {
-          display: flex;
-          flex-direction: column;
-          align-items: center;
-          justify-content: center;
-          text-align: center;
-          padding: 0.75rem 0;
-        }
-        nav.flex a .material-icons {
-          margin-bottom: 0.25rem;
-        }
-      }
-
-      /* Dark mode styles */
-      .dark {
-        color-scheme: dark;
-      }
-      .dark body {
-        background-color: #1a1a1a;
-        color: #e5e5e5;
-      }
-      .dark header {
-        background-color: #262626;
-        border-color: #404040;
-      }
-      .dark footer {
-        background-color: #262626;
-        border-color: #404040;
-      }
-      .dark .inactive-tab {
-        color: #9ca3af;
-      }
-      .dark .inactive-tab:hover {
-        color: #d1d5db;
-      }
-      .dark #player-status-bar-container {
-        background-color: #262626;
-        color: #e5e5e5;
-      }
-      .dark .bg-gray-100 {
-        background-color: #262626;
-      }
-      .dark .bg-gray-200 {
-        background-color: #404040;
-      }
-      .dark .bg-gray-300 {
-        background-color: #525252;
-      }
-      .dark .text-gray-500 {
-        color: #9ca3af;
-      }
-      .dark .text-gray-700 {
-        color: #d1d5db;
-      }
-      .dark .text-gray-800 {
-        color: #e5e5e5;
-      }
-      .dark .border-gray-200 {
-        border-color: #404040;
-      }
-      .dark .hover\:bg-gray-200:hover {
-        background-color: #404040;
-      }
-      .dark .hover\:bg-gray-300:hover {
-        background-color: #525252;
-      }
-      .dark .hover\:border-gray-300:hover {
-        border-color: #525252;
-      }
-      .dark .hover\:bg-gray-50:hover {
-        background-color: #262626;
-      }
-      .dark .bg-white {
-        background-color: #262626;
-      }
-      .dark #shutdown-button:hover {
-        background-color: #404040;
-      }
-      .dark #restart-button:hover {
-        background-color: #404040;
-      }
-      .dark #theme-toggle:hover {
-        background-color: #404040;
-      }
-      .dark .bg-gray-50 {
-        background-color: #1a1a1a;
-      }
-      .dark .text-gray-900 {
-        color: #e5e5e5;
-      }
-      .dark .text-gray-400 {
-        color: #9ca3af;
-      }
-      .dark .border-gray-300 {
-        border-color: #404040;
-      }
-      .dark .focus\:ring-offset-2 {
-        --tw-ring-offset-color: #262626;
-      }
-      .dark .shadow-sm {
-        box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
-      }
-      .dark .shadow-lg {
-        box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2);
-      }
-      .dark .shadow-xl {
-        box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
-      }
-
-      /* Existing styles */
-      .active-tab {
-        color: #0c7ff2;
-        border-color: #0c7ff2;
-      }
-      .active-tab .material-icons {
-        color: #0c7ff2;
-      }
-      .inactive-tab {
-        color: #6b7280;
-        border-color: transparent;
-      }
-      .inactive-tab:hover {
-        color: #374151;
-      }
-      #status-message {
-        transition: opacity 0.5s ease-in-out, transform 0.5s ease-in-out;
-      }
-
-      /* Switch toggle styles */
-      .switch {
-        position: relative;
-        display: inline-block;
-        width: 40px;
-        height: 20px;
-      }
-
-      .switch input {
-        opacity: 0;
-        width: 0;
-        height: 0;
-      }
-
-      .slider {
-        position: absolute;
-        cursor: pointer;
-        top: 0;
-        left: 0;
-        right: 0;
-        bottom: 0;
-        background-color: #ccc;
-        transition: .4s;
-        border-radius: 20px;
-      }
-
-      .slider:before {
-        position: absolute;
-        content: "";
-        height: 16px;
-        width: 16px;
-        left: 2px;
-        bottom: 2px;
-        background-color: white;
-        transition: .4s;
-        border-radius: 50%;
-      }
-
-      input:checked + .slider {
-        background-color: #0c7ff2;
-      }
-
-      input:checked + .slider:before {
-        transform: translateX(20px);
-      }
-
-      /* Shadow for top of status bar */
-      .shadow-lg-top {
-        box-shadow: 0 -4px 6px -1px rgb(0 0 0 / 0.1), 0 -2px 4px -2px rgb(0 0 0 / 0.1);
-      }
-
-      /* Marquee animation for pattern name on small screens */
-      @keyframes marquee {
-        0%   { transform: translateX(0%); }
-        50% { transform: translateX(-50%); }
-      }
-      .pattern-marquee {
-        display: inline-block;
-        min-width: 100%;
-        animation: marquee 8s linear infinite;
-      }
-      @media (min-width: 640px) { /* sm: and up, disable marquee */
-        .pattern-marquee {
-          animation: none !important;
-          transform: none !important;
-        }
-      }
-
-      {% block additional_styles %}{% endblock %}
-    </style>
-    {% block additional_head %}{% endblock %}
-  </head>
-  <body
-    class="bg-gray-50"
-    style="font-family: 'Plus Jakarta Sans', 'Noto Sans', sans-serif"
-  >
-  <div
-          class="mt-2 w-full text-center px-4 z-50 pointer-events-none absolute"
-          id="status-message-container"
-  >
-      <p
-              class="text-base font-semibold opacity-0 transform -translate-y-2 transition-all duration-300 ease-in-out px-10 py-2 rounded-lg shadow-lg"
-              id="status-message"
-      ></p>
-  </div>
-    <div
-      class="relative flex size-full min-h-screen flex-col group/design-root overflow-x-hidden"
-    >
-      <div class="layout-container flex min-h-full grow flex-col">
-        <header
-          class="fixed top-0 left-0 right-0 z-20 flex items-center justify-between whitespace-nowrap border-b border-solid border-b-gray-200 bg-white px-4 sm:px-6 md:px-10 py-3 sm:py-4 shadow-sm"
-        >
-          <div class="flex items-center gap-3 text-gray-800">
-            <a href="/" class="flex items-center gap-3 text-gray-800 hover:opacity-80 transition-opacity">
-            <div class="text-blue-600 w-9 h-9 rounded-full shadow overflow-hidden">
-              <img src="{% if custom_logo %}/static/custom/{{ custom_logo }}{% else %}/static/apple-touch-icon.png{% endif %}" alt="{{ app_name or 'Dune Weaver' }} Logo" class="w-full h-full object-cover"/>
-            </div>
-            <h1
-              class="text-gray-800 text-xl font-bold leading-tight tracking-tight flex items-center gap-2"
-            >
-              {{ app_name or 'Dune Weaver' }}
-              <span
-                id="connectionStatusDot"
-                class="inline-block size-2 rounded-full bg-red-500 ml-2 align-middle"
-              ></span>
-            </h1>
-            </a>
-          </div>
-          <div class="flex items-center gap-2">
-            <!-- Update Available Indicator -->
-            <button
-              id="update-indicator"
-              class="hidden p-1.5 flex rounded-lg hover:bg-green-100 dark:hover:bg-green-900 focus:outline-none focus:ring-2 focus:ring-green-500"
-              aria-label="Update available"
-              title="Software update available - Click to view"
-            >
-              <span class="material-icons text-green-600 dark:text-green-400 animate-pulse">system_update</span>
-            </button>
-
-            <button
-              id="theme-toggle"
-              class="p-1.5 flex rounded-lg hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
-              aria-label="Toggle dark mode"
-            >
-              <span class="material-icons" id="theme-toggle-icon">dark_mode</span>
-            </button>
-            <button
-              id="view-logs-button"
-              class="p-1.5 flex rounded-lg hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
-              aria-label="View logs"
-              title="View Application Logs"
-              onclick="openLogsModal()"
-            >
-              <span class="material-icons">article</span>
-            </button>
-            <button
-              id="restart-button"
-              class="p-1.5 flex rounded-lg hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-amber-500"
-              aria-label="Restart system"
-              title="Restart Docker Containers"
-            >
-              <span class="material-icons text-amber-600">restart_alt</span>
-            </button>
-            <button
-              id="shutdown-button"
-              class="p-1.5 flex rounded-lg hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-red-500"
-              aria-label="Shutdown system"
-              title="Shutdown System (Raspberry Pi only)"
-            >
-              <span class="material-icons text-red-600">power_settings_new</span>
-            </button>
-          </div>
-        </header>
-        <main class="flex flex-1 justify-center px-4 sm:px-6 lg:px-8 pt-16 sm:pt-20">
-          {% block content %}{% endblock %}
-        </main>
-
-        <!-- Floating Preview Button -->
-        <button
-          id="toggle-preview-modal-btn"
-          class="fixed bottom-24 right-4 z-30 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg w-12 h-12 flex items-center justify-center transition-all focus:outline-none focus:ring-2 focus:ring-blue-500"
-          title="Toggle Pattern Preview"
-        >
-          <span class="material-icons text-xl">preview</span>
-        </button>
-
-        <footer
-          class="fixed bottom-0 left-0 right-0 z-20 bg-white border-t border-gray-200 shadow-t-sm"
-        >
-          <nav class="mx-auto flex max-w-5xl justify-around">
-            <a
-              class="{% if request.url.path == '/' %}active-tab{% else %}inactive-tab{% endif %} flex flex-1 flex-col items-center justify-center gap-1 border-b-[3px] {% if request.url.path != '/' %}border-transparent{% endif %} py-3 text-xs font-medium sm:text-sm"
-              href="/"
-            >
-              <span class="material-icons">search</span> Browse
-            </a>
-            <a
-              class="{% if request.url.path == '/playlists' %}active-tab{% else %}inactive-tab{% endif %} flex flex-1 flex-col items-center justify-center gap-1 border-b-[3px] py-3 text-xs font-medium sm:text-sm"
-              href="/playlists"
-            >
-              <span class="material-icons">list_alt</span> Playlists
-            </a>
-            <a
-              class="{% if request.url.path == '/table_control' %}active-tab{% else %}inactive-tab{% endif %} flex flex-1 flex-col items-center justify-center gap-1 border-b-[3px] py-3 text-xs font-medium sm:text-sm"
-              href="/table_control"
-            >
-              <span class="material-icons">table_chart</span> Table Control
-            </a>
-            <a
-            class="{% if request.url.path == '/led' %}active-tab{% else %}inactive-tab{% endif %} flex flex-1 flex-col items-center justify-center gap-1 border-b-[3px] py-3 text-xs font-medium sm:text-sm"
-            href="/led"
-          >
-            <span class="material-icons">lightbulb</span> <span id="led-nav-label">LED</span>
-          </a>
-            <a
-              class="{% if request.url.path == '/settings' %}active-tab{% else %}inactive-tab{% endif %} flex flex-1 flex-col items-center justify-center gap-1 border-b-[3px] py-3 text-xs font-medium sm:text-sm"
-              href="/settings"
-            >
-              <span class="material-icons">settings</span> Settings
-            </a>
-
-          </nav>
-        </footer>
-      </div>
-    </div>
-    {% block scripts %}{% endblock %}
-    <script src="/static/js/base.js"></script>
-
-    <!-- Cache Progress Modal -->
-    <div id="cacheProgressModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden p-4">
-        <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md">
-            <div class="p-6">
-                <div class="text-center">
-                    <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-2">Initializing Pattern Cache</h2>
-                    <p class="text-gray-600 dark:text-gray-400 mb-4">Preparing your pattern previews...</p>
-                    
-                    <div class="mb-4">
-                        <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
-                            <div id="cacheProgressBar" class="bg-blue-600 dark:bg-blue-400 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
-                        </div>
-                        <div class="mt-2 flex justify-between text-sm text-gray-500 dark:text-gray-400">
-                            <span id="cacheProgressText">Starting...</span>
-                            <span id="cacheProgressPercentage">0%</span>
-                        </div>
-                    </div>
-                    
-                    <div class="text-sm text-gray-500 dark:text-gray-400">
-                        <p id="cacheCurrentStage">Preparing...</p>
-                        <p id="cacheCurrentFile" class="mt-1 truncate"></p>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <!-- Player Preview Modal -->
-    <div id="playerPreviewModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-40 hidden p-4">
-        <div class="bg-white rounded-lg shadow-xl w-full max-w-5xl max-h-[95vh] flex flex-col">
-            <div class="flex items-center justify-between p-4 border-b border-gray-200 flex-shrink-0">
-                <h2 id="playerPreviewTitle" class="text-xl font-semibold text-gray-800">Pattern Preview</h2>
-                <button id="closePlayerPreview" class="text-gray-400 hover:text-gray-600 transition-colors">
-                    <span class="material-icons text-2xl">close</span>
-                </button>
-            </div>
-
-            <!-- Canvas Area -->
-            <div class="flex-1 p-4 flex justify-center items-center overflow-hidden">
-                <div class="relative flex items-center justify-center w-full h-full">
-                    <div class="relative max-w-[min(800px,90vw)] max-h-[min(600px,80vh)] aspect-square">
-                        <canvas id="playerPreviewCanvas" width="800" height="800" class="w-full h-full rounded-full border border-gray-300 dark:invert"></canvas>
-                    </div>
-                </div>
-            </div>
-
-            <!-- Pattern Info and Controls -->
-            <div class="flex-shrink-0 p-4 border-b border-gray-200 flex items-center gap-4 flex-wrap">
-                <!-- Pattern Preview Image -->
-                <div class="flex-shrink-0">
-                    <img id="modal-pattern-preview-img"
-                         class="w-[126px] h-[126px] rounded-full border border-gray-300 object-cover dark:invert" src="">
-                </div>
-
-                <div class="flex-grow w-auto">
-                    <div class="flex items-center justify-between mb-2">
-                        <div class="min-w-0 max-w-[50%]">
-                            <div class="overflow-hidden relative">
-                                <p class="text-lg font-semibold whitespace-nowrap" id="modal-pattern-name">
-                                    No pattern playing
-                                </p>
-                            </div>
-                            <p class="text-base text-gray-500" id="modal-eta">
-                                ETA: --:--
-                            </p>
-                        </div>
-                        <div class="flex items-center gap-2 flex-shrink-0">
-                            <button
-                                aria-label="Next"
-                                class="w-12 aspect-square rounded-full flex items-center justify-center hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
-                                id="modal-skip-button"
-                            >
-                                <span class="material-icons text-gray-700">skip_next</span>
-                            </button>
-                            <button
-                                aria-label="Pause"
-                                class="w-12 aspect-square rounded-full flex items-center justify-center hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
-                                id="modal-pause-button"
-                            >
-                                <span class="material-icons text-gray-700">pause</span>
-                            </button>
-                            <button
-                                aria-label="Stop"
-                                class="w-12 aspect-square rounded-full flex items-center justify-center hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-blue-500"
-                                id="modal-stop-button"
-                            >
-                                <span class="material-icons text-gray-700">stop</span>
-                            </button>
-                        </div>
-                    </div>
-                    <div class="w-full bg-gray-300 rounded-full h-2.5 mb-2">
-                        <div
-                            class="bg-blue-600 h-2.5 rounded-full"
-                            id="modal-progress-bar"
-                            style="width: 0%"
-                        ></div>
-                    </div>
-                    <div class="flex items-center justify-between">
-                        <div class="text-base text-gray-500">
-                            Next:
-                            <span class="text-gray-700 font-medium" id="modal-next-pattern">None</span>
-                        </div>
-                        <div class="flex items-center gap-3 text-sm">
-                            <span class="text-gray-500">Speed:</span>
-                            <div class="flex items-center gap-2">
-                                <div class="relative min-w-[4ch] text-center cursor-pointer inline-flex items-center" id="modal-speed-container">
-                                    <span class="inline-block px-2 py-1 text-gray-700 font-medium border border-transparent rounded hover:border-gray-300 hover:bg-gray-50 transition-colors" id="modal-speed-display" title="Click to edit speed">--</span>
-                                    <input type="number" min="1" max="5000" step="1" class="w-16 sm:w-20 px-2 py-1 text-sm border border-blue-500 rounded focus:border-blue-500 focus:ring-1 focus:ring-blue-500 text-center hidden" id="modal-speed-input" />
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <script>
-      // Function to show status message
-      function showStatusMessage(message, type = "success") {
-        const statusContainer = document.getElementById(
-          "status-message-container"
-        );
-        const statusMessage = document.getElementById("status-message");
-
-        if (!statusContainer || !statusMessage) return;
-
-        // Set message and color based on type
-        statusMessage.textContent = message;
-        statusMessage.className = `text-base font-semibold opacity-0 transform -translate-y-2 transition-all duration-300 ease-in-out px-4 py-2 rounded-lg shadow-lg ${
-          type === "success"
-            ? "bg-green-50 text-green-700 border border-green-200"
-            : type === "error"
-            ? "bg-red-50 text-red-700 border border-red-200"
-            : type === "warning"
-            ? "bg-yellow-50 text-yellow-700 border border-yellow-200"
-            : "bg-blue-50 text-blue-700 border border-blue-200"
-        }`;
-
-        // Show message with animation
-        requestAnimationFrame(() => {
-          statusMessage.classList.remove("opacity-0", "-translate-y-2");
-          statusMessage.classList.add("opacity-100", "translate-y-0");
-        });
-
-        // Hide message after 5 seconds
-        setTimeout(() => {
-          statusMessage.classList.remove("opacity-100", "translate-y-0");
-          statusMessage.classList.add("opacity-0", "-translate-y-2");
-        }, 5000);
-      }
-
-      // Initial connection status check
-      async function checkInitialConnectionStatus() {
-        try {
-          const response = await fetch("/serial_status");
-          if (response.ok) {
-            const data = await response.json();
-            const statusDot = document.getElementById("connectionStatusDot");
-            if (statusDot) {
-              statusDot.className = `inline-block size-2 rounded-full ml-2 align-middle ${
-                data.connected ? "bg-green-500" : "bg-red-500"
-              }`;
-            }
-          }
-        } catch (error) {
-          console.error("Error checking initial connection status:", error);
-        }
-      }
-
-      // Check initial status on page load
-      document.addEventListener("DOMContentLoaded", checkInitialConnectionStatus);
-
-      // Player bar toggle logic will be handled by base.js
-    </script>
-    
-    <script>
-      // Cache progress modal functionality
-      let cacheProgressSocket = null;
-      let cacheProgressCheckInterval = null;
-      
-      function initCacheProgressModal() {
-        const modal = document.getElementById('cacheProgressModal');
-        const progressBar = document.getElementById('cacheProgressBar');
-        const progressText = document.getElementById('cacheProgressText');
-        const progressPercentage = document.getElementById('cacheProgressPercentage');
-        const currentStage = document.getElementById('cacheCurrentStage');
-        const currentFile = document.getElementById('cacheCurrentFile');
-        
-        // Check if cache generation is needed on page load
-        fetch('/cache-progress')
-          .then(response => response.json())
-          .then(data => {
-            if (data.is_running) {
-              showCacheProgressModal();
-            }
-          })
-          .catch(error => {
-            console.error('Error checking initial cache progress:', error);
-          });
-        
-        function showCacheProgressModal() {
-          modal.classList.remove('hidden');
-          
-          // Connect to WebSocket for real-time updates
-          const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
-          const wsUrl = `${protocol}//${window.location.host}/ws/cache-progress`;
-          
-          cacheProgressSocket = new WebSocket(wsUrl);
-          
-          cacheProgressSocket.onmessage = function(event) {
-            const data = JSON.parse(event.data);
-            if (data.type === 'cache_progress') {
-              updateCacheProgress(data.data);
-            }
-          };
-          
-          cacheProgressSocket.onclose = function() {
-            // Fallback to polling if WebSocket fails
-            startProgressPolling();
-          };
-          
-          cacheProgressSocket.onerror = function() {
-            // Fallback to polling if WebSocket fails
-            startProgressPolling();
-          };
-        }
-        
-        function startProgressPolling() {
-          if (cacheProgressCheckInterval) {
-            clearInterval(cacheProgressCheckInterval);
-          }
-          
-          cacheProgressCheckInterval = setInterval(() => {
-            fetch('/cache-progress')
-              .then(response => response.json())
-              .then(data => updateCacheProgress(data))
-              .catch(error => console.error('Error fetching cache progress:', error));
-          }, 1000);
-        }
-        
-        function updateCacheProgress(data) {
-          if (!data.is_running && data.stage === 'complete') {
-            console.log('Initial cache generation completed');
-            hideCacheProgressModal();
-            // Trigger cache all prompt after initial cache generation completes
-            if (typeof onInitialCacheComplete === 'function') {
-              console.log('Calling onInitialCacheComplete()');
-              onInitialCacheComplete();
-            } else {
-              console.log('onInitialCacheComplete function not found');
-            }
-            return;
-          }
-          
-          if (!data.is_running && data.stage === 'error') {
-            progressText.textContent = 'Error occurred';
-            currentStage.textContent = data.error || 'Unknown error';
-            currentFile.textContent = '';
-            return;
-          }
-          
-          if (!data.is_running) {
-            hideCacheProgressModal();
-            return;
-          }
-          
-          // Calculate progress percentage
-          const percentage = data.total_files > 0 ? Math.round((data.processed_files / data.total_files) * 100) : 0;
-          
-          // Update progress bar
-          progressBar.style.width = `${percentage}%`;
-          progressPercentage.textContent = `${percentage}%`;
-          
-          // Update text based on stage
-          let stageText = '';
-          let progressTextContent = '';
-          
-          switch (data.stage) {
-            case 'starting':
-              stageText = 'Initializing...';
-              progressTextContent = 'Getting ready...';
-              break;
-            case 'metadata':
-              stageText = 'Processing pattern metadata';
-              progressTextContent = `${data.processed_files} of ${data.total_files} patterns`;
-              break;
-            case 'images':
-              stageText = 'Generating pattern previews';
-              progressTextContent = `${data.processed_files} of ${data.total_files} previews`;
-              break;
-            default:
-              stageText = 'Processing...';
-              progressTextContent = `${data.processed_files} of ${data.total_files} files`;
-          }
-          
-          currentStage.textContent = stageText;
-          progressText.textContent = progressTextContent;
-          
-          // Update current file
-          if (data.current_file) {
-            currentFile.textContent = `Current: ${data.current_file}`;
-          } else {
-            currentFile.textContent = '';
-          }
-        }
-        
-        function hideCacheProgressModal() {
-          modal.classList.add('hidden');
-          
-          if (cacheProgressSocket) {
-            cacheProgressSocket.close();
-            cacheProgressSocket = null;
-          }
-          
-          if (cacheProgressCheckInterval) {
-            clearInterval(cacheProgressCheckInterval);
-            cacheProgressCheckInterval = null;
-          }
-        }
-        
-        // Expose functions globally for potential external use
-        window.showCacheProgressModal = showCacheProgressModal;
-        window.hideCacheProgressModal = hideCacheProgressModal;
-      }
-      
-      // Initialize when DOM is loaded
-      document.addEventListener('DOMContentLoaded', initCacheProgressModal);
-    </script>
-    <script>
-      // Theme switching functionality
-      document.addEventListener('DOMContentLoaded', function() {
-        const themeToggle = document.getElementById('theme-toggle');
-        const themeToggleIcon = document.getElementById('theme-toggle-icon');
-
-        // Update icon based on current theme
-        function updateThemeIcon() {
-          const isDark = document.documentElement.classList.contains('dark');
-          themeToggleIcon.textContent = isDark ? 'light_mode' : 'dark_mode';
-        }
-
-        // Initial icon update
-        updateThemeIcon();
-
-        // Theme toggle click handler
-        themeToggle.addEventListener('click', () => {
-          const isDark = document.documentElement.classList.toggle('dark');
-          localStorage.setItem('theme', isDark ? 'dark' : 'light');
-          updateThemeIcon();
-        });
-      });
-
-      // Shutdown button functionality
-      document.addEventListener('DOMContentLoaded', function() {
-        const shutdownButton = document.getElementById('shutdown-button');
-
-        // Shutdown button click handler
-        shutdownButton.addEventListener('click', async () => {
-          const confirmed = confirm('Are you sure you want to shutdown the system?\n\nNote: This only works on Raspberry Pi.\n\nThis will:\n1. Stop Docker containers\n2. Shut down the system\n\nYou will need physical access to restart it.');
-
-          if (!confirmed) return;
-
-          try {
-            showStatusMessage('Initiating shutdown...', 'warning');
-
-            const response = await fetch('/api/system/shutdown', { method: 'POST' });
-            const data = await response.json();
-
-            if (data.success) {
-              showStatusMessage('System is shutting down...', 'success');
-            } else {
-              showStatusMessage('Shutdown failed: ' + data.message, 'error');
-            }
-          } catch (error) {
-            showStatusMessage('Failed to shutdown: ' + error.message, 'error');
-          }
-        });
-      });
-
-      // Restart button functionality
-      document.addEventListener('DOMContentLoaded', function() {
-        const restartButton = document.getElementById('restart-button');
-
-        // Restart button click handler
-        restartButton.addEventListener('click', async () => {
-          const confirmed = confirm('Are you sure you want to restart the application?\n\nThis will restart the Docker containers.\nThe page will reload automatically when the service is back online.');
-
-          if (!confirmed) return;
-
-          try {
-            showStatusMessage('Initiating restart...', 'warning');
-
-            const response = await fetch('/api/system/restart', { method: 'POST' });
-            const data = await response.json();
-
-            if (data.success) {
-              showStatusMessage('System is restarting... Page will reload when ready.', 'success');
-
-              // Start checking if the server is back online
-              setTimeout(() => {
-                checkServerAndReload();
-              }, 3000);
-            } else {
-              showStatusMessage('Restart failed: ' + data.message, 'error');
-            }
-          } catch (error) {
-            showStatusMessage('Failed to restart: ' + error.message, 'error');
-          }
-        });
-
-        // Function to check if server is back online and reload
-        function checkServerAndReload() {
-          const checkInterval = setInterval(async () => {
-            try {
-              const response = await fetch('/api/version', { method: 'GET' });
-              if (response.ok) {
-                clearInterval(checkInterval);
-                showStatusMessage('Server is back online. Reloading...', 'success');
-                setTimeout(() => {
-                  window.location.reload();
-                }, 1000);
-              }
-            } catch (error) {
-              // Server not ready yet, keep checking
-              console.log('Server not ready yet, retrying...');
-            }
-          }, 2000);
-
-          // Stop checking after 60 seconds
-          setTimeout(() => {
-            clearInterval(checkInterval);
-          }, 60000);
-        }
-      });
-
-      // Update indicator functionality
-      document.addEventListener('DOMContentLoaded', async function() {
-        const updateIndicator = document.getElementById('update-indicator');
-        if (!updateIndicator) return;
-
-        // Check for updates
-        async function checkForUpdates() {
-          try {
-            const response = await fetch('/api/version');
-            const data = await response.json();
-
-            // Show indicator if update is available
-            if (data.update_available) {
-              updateIndicator.classList.remove('hidden');
-            } else {
-              updateIndicator.classList.add('hidden');
-            }
-          } catch (error) {
-            console.error('Failed to check for updates:', error);
-          }
-        }
-
-        // Initial check
-        await checkForUpdates();
-
-        // Check every hour
-        setInterval(checkForUpdates, 3600000);
-
-        // Click handler - navigate to settings and scroll to update section
-        updateIndicator.addEventListener('click', () => {
-          // Navigate to settings page
-          window.location.href = '/settings#software-version-section';
-        });
-      });
-    </script>
-
-    <!-- Cache All Previews Prompt Modal -->
-    <div id="cacheAllPromptModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden p-4">
-        <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md">
-            <div class="p-6">
-                <div class="text-center">
-                    <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-2">Cache All Pattern Previews?</h2>
-                    <p class="text-gray-600 dark:text-gray-400 mb-4 max-w-md mx-auto">
-                        Would you like to cache all pattern previews for faster browsing? This will download and store preview images in your browser for instant loading.
-                    </p>
-                    
-                    <div class="bg-amber-50 dark:bg-amber-900 p-3 rounded-lg mb-4 text-sm">
-                        <p class="text-amber-700 dark:text-amber-300">
-                            <strong>Note:</strong> This cache is browser-specific. You'll need to repeat this process for each browser you use.
-                        </p>
-                    </div>
-                    
-                    <!-- Progress section (hidden initially) -->
-                    <div id="cacheAllProgress" class="mb-4 hidden">
-                        <div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
-                            <div id="cacheAllProgressBar" class="bg-blue-600 dark:bg-blue-400 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
-                        </div>
-                        <div class="mt-2 flex justify-between text-sm text-gray-500 dark:text-gray-400">
-                            <span id="cacheAllProgressText">Starting...</span>
-                            <span id="cacheAllProgressPercentage">0%</span>
-                        </div>
-                    </div>
-                    
-                    <!-- Buttons -->
-                    <div id="cacheAllButtons" class="flex gap-3 justify-center">
-                        <button id="skipCacheAllBtn" class="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 transition-colors">
-                            Skip for now
-                        </button>
-                        <button id="startCacheAllBtn" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
-                            Cache All Previews
-                        </button>
-                    </div>
-                    
-                    <!-- Completion message (hidden initially) -->
-                    <div id="cacheAllComplete" class="hidden">
-                        <p class="text-green-600 dark:text-green-400 mb-4">✓ All previews cached successfully!</p>
-                        <button id="closeCacheAllBtn" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
-                            Done
-                        </button>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <!-- Logs Bottom Panel -->
-    <div id="logsPanel" class="fixed left-0 right-0 z-20 bg-white dark:bg-gray-800 shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)] flex flex-col transition-transform duration-300 ease-out" style="bottom: 0; height: 45vh; transform: translateY(100%);">
-      <!-- Panel Header -->
-      <div class="flex items-center justify-between px-4 py-2 border-b border-slate-200 dark:border-gray-600 shrink-0">
-        <div class="flex items-center gap-2">
-          <span class="material-icons text-lg text-slate-600 dark:text-gray-300">article</span>
-          <h3 class="text-sm font-semibold text-slate-800 dark:text-gray-100">Logs</h3>
-          <span id="logsConnectionStatus" class="text-xs px-2 py-0.5 rounded-full bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-300">Connecting...</span>
-        </div>
-        <div class="flex items-center gap-2">
-          <select id="logLevelFilter" onchange="filterLogs()" class="form-select text-xs rounded border-slate-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 py-1 pl-2 pr-8">
-            <option value="">All</option>
-            <option value="DEBUG">Debug</option>
-            <option value="INFO">Info</option>
-            <option value="WARNING">Warn</option>
-            <option value="ERROR">Error</option>
-            <option value="CRITICAL">Critical</option>
-          </select>
-          <button
-            onclick="clearLogs()"
-            class="flex items-center justify-center rounded h-7 px-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-500 text-gray-700 dark:text-gray-200 text-xs font-medium transition-colors"
-            title="Clear logs"
-          >
-            <span class="material-icons text-sm">delete_sweep</span>
-          </button>
-          <label class="flex items-center gap-1 text-xs text-slate-600 dark:text-gray-300">
-            <input type="checkbox" id="logsAutoScroll" checked class="rounded border-slate-300 dark:border-gray-600 w-3.5 h-3.5">
-            Auto
-          </label>
-          <button
-            onclick="closeLogsModal()"
-            class="flex items-center justify-center rounded size-7 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-500 dark:text-gray-400 transition-colors"
-            title="Close logs"
-          >
-            <span class="material-icons text-lg">close</span>
-          </button>
-        </div>
-      </div>
-      <!-- Panel Body -->
-      <div id="logsContainer" class="flex-1 overflow-y-auto p-2 font-mono text-xs bg-slate-50 dark:bg-gray-900">
-        <div id="logsContent" class="space-y-0.5">
-          <!-- Log entries will be inserted here -->
-        </div>
-      </div>
-      <!-- Panel Footer -->
-      <div class="px-4 py-1.5 border-t border-slate-200 dark:border-gray-600 flex items-center justify-between text-xs text-slate-500 dark:text-gray-400 shrink-0">
-        <span id="logsCount">0 entries</span>
-        <span id="logsInfo">Last 500 entries</span>
-      </div>
-    </div>
-
-    <script>
-    // ============================================================================
-    // Application Logs Bottom Panel
-    // ============================================================================
-    let logsWebSocket = null;
-    let logsEntries = [];
-    let logsPanelOpen = false;
-    const MAX_LOG_ENTRIES = 500;
-    const LOGS_PANEL_HEIGHT = '45vh';
-
-    const LOG_LEVEL_COLORS = {
-        DEBUG: 'text-gray-500 dark:text-gray-400',
-        INFO: 'text-blue-600 dark:text-blue-400',
-        WARNING: 'text-amber-600 dark:text-amber-400',
-        ERROR: 'text-red-600 dark:text-red-400',
-        CRITICAL: 'text-red-700 dark:text-red-300 font-bold'
-    };
-
-    const LOG_LEVEL_BG = {
-        DEBUG: 'bg-gray-100 dark:bg-gray-700',
-        INFO: 'bg-blue-50 dark:bg-blue-900/30',
-        WARNING: 'bg-amber-50 dark:bg-amber-900/30',
-        ERROR: 'bg-red-50 dark:bg-red-900/30',
-        CRITICAL: 'bg-red-100 dark:bg-red-900/50'
-    };
-
-    function openLogsModal(skipSave = false) {
-        if (logsPanelOpen) return;
-        logsPanelOpen = true;
-
-        const panel = document.getElementById('logsPanel');
-        const footer = document.querySelector('footer');
-        const previewBtn = document.getElementById('toggle-preview-modal-btn');
-
-        // Slide panel up
-        panel.style.transform = 'translateY(0)';
-
-        // Push footer up
-        if (footer) {
-            footer.style.transition = 'transform 0.3s ease-out';
-            footer.style.transform = `translateY(-${LOGS_PANEL_HEIGHT})`;
-        }
-
-        // Push floating preview button up
-        if (previewBtn) {
-            previewBtn.style.transition = 'transform 0.3s ease-out';
-            previewBtn.style.transform = `translateY(-${LOGS_PANEL_HEIGHT})`;
-        }
-
-        // Persist state across pages
-        if (!skipSave) {
-            localStorage.setItem('logsPanelOpen', 'true');
-        }
-
-        loadInitialLogs();
-        connectLogsWebSocket();
-        document.addEventListener('keydown', handleLogsEscapeKey);
-    }
-
-    function closeLogsModal() {
-        if (!logsPanelOpen) return;
-        logsPanelOpen = false;
-
-        const panel = document.getElementById('logsPanel');
-        const footer = document.querySelector('footer');
-        const previewBtn = document.getElementById('toggle-preview-modal-btn');
-
-        // Slide panel down
-        panel.style.transform = 'translateY(100%)';
-
-        // Reset footer position
-        if (footer) {
-            footer.style.transform = 'translateY(0)';
-        }
-
-        // Reset floating button position
-        if (previewBtn) {
-            previewBtn.style.transform = 'translateY(0)';
-        }
-
-        // Persist state across pages
-        localStorage.removeItem('logsPanelOpen');
-
-        if (logsWebSocket) {
-            logsWebSocket.close();
-            logsWebSocket = null;
-        }
-        document.removeEventListener('keydown', handleLogsEscapeKey);
-    }
-
-    function handleLogsEscapeKey(e) {
-        if (e.key === 'Escape') closeLogsModal();
-    }
-
-    // Restore logs panel state on page load
-    document.addEventListener('DOMContentLoaded', function() {
-        if (localStorage.getItem('logsPanelOpen') === 'true') {
-            // Small delay to ensure DOM is ready
-            setTimeout(() => openLogsModal(true), 100);
-        }
-    });
-
-    async function loadInitialLogs() {
-        const logsContent = document.getElementById('logsContent');
-        logsContent.innerHTML = '<div class="text-gray-500 dark:text-gray-400">Loading logs...</div>';
-        try {
-            const response = await fetch('/api/logs?limit=500');
-            const data = await response.json();
-            logsEntries = data.logs || [];
-            renderLogs();
-            updateLogsCount();
-            const container = document.getElementById('logsContainer');
-            container.scrollTop = container.scrollHeight;
-        } catch (error) {
-            console.error('Failed to load logs:', error);
-            logsContent.innerHTML = '<div class="text-red-500">Failed to load logs</div>';
-        }
-    }
-
-    function connectLogsWebSocket() {
-        const statusEl = document.getElementById('logsConnectionStatus');
-        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
-        const wsUrl = `${protocol}//${window.location.host}/ws/logs`;
-        try {
-            logsWebSocket = new WebSocket(wsUrl);
-            logsWebSocket.onopen = () => {
-                statusEl.textContent = 'Live';
-                statusEl.className = 'text-xs px-2 py-1 rounded-full bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300';
-            };
-            logsWebSocket.onmessage = (event) => {
-                const message = JSON.parse(event.data);
-                if (message.type === 'log_entry') addLogEntry(message.data);
-            };
-            logsWebSocket.onclose = () => {
-                statusEl.textContent = 'Disconnected';
-                statusEl.className = 'text-xs px-2 py-1 rounded-full bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-300';
-            };
-            logsWebSocket.onerror = (error) => {
-                console.error('Logs WebSocket error:', error);
-                statusEl.textContent = 'Error';
-                statusEl.className = 'text-xs px-2 py-1 rounded-full bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300';
-            };
-        } catch (error) {
-            console.error('Failed to connect to logs WebSocket:', error);
-            statusEl.textContent = 'Failed';
-            statusEl.className = 'text-xs px-2 py-1 rounded-full bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300';
-        }
-    }
-
-    function addLogEntry(entry) {
-        logsEntries.unshift(entry);
-        if (logsEntries.length > MAX_LOG_ENTRIES) logsEntries.pop();
-        const logsContent = document.getElementById('logsContent');
-        const entryEl = createLogEntryElement(entry);
-        logsContent.appendChild(entryEl);
-        const autoScroll = document.getElementById('logsAutoScroll').checked;
-        if (autoScroll) {
-            const container = document.getElementById('logsContainer');
-            container.scrollTop = container.scrollHeight;
-        }
-        const levelFilter = document.getElementById('logLevelFilter').value;
-        if (levelFilter && entry.level !== levelFilter) entryEl.classList.add('hidden');
-        updateLogsCount();
-    }
-
-    function createLogEntryElement(entry) {
-        const div = document.createElement('div');
-        div.className = `log-entry flex gap-2 py-1 px-2 rounded ${LOG_LEVEL_BG[entry.level] || 'bg-gray-50 dark:bg-gray-800'}`;
-        div.dataset.level = entry.level;
-        const timestamp = new Date(entry.timestamp);
-        const timeStr = timestamp.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
-        const msStr = timestamp.getMilliseconds().toString().padStart(3, '0');
-        div.innerHTML = `
-            <span class="text-gray-400 dark:text-gray-500 shrink-0">${timeStr}.${msStr}</span>
-            <span class="${LOG_LEVEL_COLORS[entry.level] || 'text-gray-600'} shrink-0 w-16">${entry.level}</span>
-            <span class="text-purple-600 dark:text-purple-400 shrink-0 truncate max-w-32" title="${entry.logger}:${entry.line}">${entry.module}:${entry.line}</span>
-            <span class="text-slate-700 dark:text-gray-200 break-all">${escapeHtmlLogs(entry.message)}</span>
-        `;
-        return div;
-    }
-
-    function escapeHtmlLogs(text) {
-        const div = document.createElement('div');
-        div.textContent = text;
-        return div.innerHTML;
-    }
-
-    function renderLogs() {
-        const logsContent = document.getElementById('logsContent');
-        logsContent.innerHTML = '';
-        const reversedLogs = [...logsEntries].reverse();
-        const levelFilter = document.getElementById('logLevelFilter').value;
-        for (const entry of reversedLogs) {
-            const entryEl = createLogEntryElement(entry);
-            if (levelFilter && entry.level !== levelFilter) entryEl.classList.add('hidden');
-            logsContent.appendChild(entryEl);
-        }
-    }
-
-    function filterLogs() {
-        const levelFilter = document.getElementById('logLevelFilter').value;
-        const entries = document.querySelectorAll('#logsContent .log-entry');
-        let visibleCount = 0;
-        entries.forEach(entry => {
-            if (!levelFilter || entry.dataset.level === levelFilter) {
-                entry.classList.remove('hidden');
-                visibleCount++;
-            } else {
-                entry.classList.add('hidden');
-            }
-        });
-        const countEl = document.getElementById('logsCount');
-        if (levelFilter) {
-            countEl.textContent = `${visibleCount} of ${logsEntries.length} entries (filtered)`;
-        } else {
-            countEl.textContent = `${logsEntries.length} entries`;
-        }
-    }
-
-    function updateLogsCount() {
-        const countEl = document.getElementById('logsCount');
-        const levelFilter = document.getElementById('logLevelFilter').value;
-        if (levelFilter) {
-            const filteredCount = logsEntries.filter(e => e.level === levelFilter).length;
-            countEl.textContent = `${filteredCount} of ${logsEntries.length} entries (filtered)`;
-        } else {
-            countEl.textContent = `${logsEntries.length} entries`;
-        }
-    }
-
-    async function clearLogs() {
-        try {
-            await fetch('/api/logs', { method: 'DELETE' });
-            logsEntries = [];
-            document.getElementById('logsContent').innerHTML = '<div class="text-gray-500 dark:text-gray-400 text-center py-4">Logs cleared</div>';
-            updateLogsCount();
-        } catch (error) {
-            console.error('Failed to clear logs:', error);
-        }
-    }
-    </script>
-  </body>
-</html>

+ 0 - 178
templates/design.html

@@ -1,178 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Design - {{ app_name or 'Dune Weaver' }}{% endblock %}
-
-{% block additional_styles %}
-.design-canvas {
-  background-color: #f8fafc;
-  background-image: linear-gradient(#e2e8f0 1px, transparent 1px),
-    linear-gradient(90deg, #e2e8f0 1px, transparent 1px);
-  background-size: 20px 20px;
-}
-.design-canvas.drawing {
-  cursor: crosshair;
-}
-.design-canvas.moving {
-  cursor: grab;
-}
-.design-canvas.moving:active {
-  cursor: grabbing;
-}
-{% endblock %}
-
-{% block content %}
-<div class="layout-content-container flex flex-col w-full max-w-7xl gap-8">
-  <div class="flex flex-wrap justify-between items-center gap-4 p-4 bg-white rounded-xl shadow-sm">
-    <h1 class="text-slate-900 tracking-tight text-2xl sm:text-3xl font-bold leading-tight">
-      Design
-    </h1>
-    <div class="flex items-center gap-3">
-      <button
-        id="undoButton"
-        class="flex items-center justify-center rounded-lg h-10 px-4 bg-slate-100 hover:bg-slate-200 text-slate-700 transition-colors"
-        disabled
-      >
-        <span class="material-icons text-xl">undo</span>
-      </button>
-      <button
-        id="redoButton"
-        class="flex items-center justify-center rounded-lg h-10 px-4 bg-slate-100 hover:bg-slate-200 text-slate-700 transition-colors"
-        disabled
-      >
-        <span class="material-icons text-xl">redo</span>
-      </button>
-      <button
-        id="clearButton"
-        class="flex items-center justify-center rounded-lg h-10 px-4 bg-slate-100 hover:bg-slate-200 text-slate-700 transition-colors"
-      >
-        <span class="material-icons text-xl">delete</span>
-      </button>
-    </div>
-  </div>
-  <div class="flex flex-col lg:flex-row gap-8">
-    <div class="flex-1">
-      <div class="bg-white rounded-xl shadow-sm overflow-hidden">
-        <div class="p-4 border-b border-slate-200">
-          <div class="flex items-center justify-between">
-            <h2 class="text-slate-800 text-xl font-semibold leading-tight tracking-[-0.01em]">
-              Canvas
-            </h2>
-            <div class="flex items-center gap-2">
-              <button
-                id="zoomOutButton"
-                class="flex items-center justify-center rounded-lg h-9 px-3 bg-slate-100 hover:bg-slate-200 text-slate-700 transition-colors"
-              >
-                <span class="material-icons text-xl">remove</span>
-              </button>
-              <span id="zoomLevel" class="text-slate-700 text-sm font-medium">100%</span>
-              <button
-                id="zoomInButton"
-                class="flex items-center justify-center rounded-lg h-9 px-3 bg-slate-100 hover:bg-slate-200 text-slate-700 transition-colors"
-              >
-                <span class="material-icons text-xl">add</span>
-              </button>
-            </div>
-          </div>
-        </div>
-        <div class="p-4">
-          <div class="relative aspect-square w-full overflow-hidden rounded-lg border border-slate-200">
-            <canvas
-              id="designCanvas"
-              class="design-canvas absolute inset-0 size-full"
-              width="1000"
-              height="1000"
-            ></canvas>
-          </div>
-        </div>
-      </div>
-    </div>
-    <div class="w-full lg:w-80">
-      <div class="bg-white rounded-xl shadow-sm overflow-hidden">
-        <div class="p-4 border-b border-slate-200">
-          <h2 class="text-slate-800 text-xl font-semibold leading-tight tracking-[-0.01em]">
-            Tools
-          </h2>
-        </div>
-        <div class="p-4 space-y-4">
-          <div class="space-y-2">
-            <label class="text-slate-700 text-sm font-medium leading-normal">Mode</label>
-            <div class="grid grid-cols-2 gap-2">
-              <label class="group relative flex cursor-pointer items-center justify-center rounded-lg border border-slate-300 p-2 text-center text-sm font-medium text-slate-700 transition-all hover:border-sky-500 has-[:checked]:border-sky-500 has-[:checked]:bg-sky-500 has-[:checked]:text-white has-[:checked]:ring-2 has-[:checked]:ring-sky-500 has-[:checked]:ring-offset-2">
-                Draw
-                <input checked class="peer sr-only" name="mode" type="radio" value="draw" />
-              </label>
-              <label class="group relative flex cursor-pointer items-center justify-center rounded-lg border border-slate-300 p-2 text-center text-sm font-medium text-slate-700 transition-all hover:border-sky-500 has-[:checked]:border-sky-500 has-[:checked]:bg-sky-500 has-[:checked]:text-white has-[:checked]:ring-2 has-[:checked]:ring-sky-500 has-[:checked]:ring-offset-2">
-                Move
-                <input class="peer sr-only" name="mode" type="radio" value="move" />
-              </label>
-            </div>
-          </div>
-          <div class="space-y-2">
-            <label class="text-slate-700 text-sm font-medium leading-normal">Brush Size</label>
-            <input
-              id="brushSizeInput"
-              class="form-range w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer"
-              type="range"
-              min="1"
-              max="50"
-              value="5"
-            />
-            <div class="flex justify-between text-xs text-slate-500">
-              <span>1px</span>
-              <span>50px</span>
-            </div>
-          </div>
-          <div class="space-y-2">
-            <label class="text-slate-700 text-sm font-medium leading-normal">Color</label>
-            <div class="grid grid-cols-4 gap-2">
-              <button
-                class="size-8 rounded-full bg-black border-2 border-slate-300 hover:border-slate-400 transition-colors"
-                data-color="#000000"
-              ></button>
-              <button
-                class="size-8 rounded-full bg-white border-2 border-slate-300 hover:border-slate-400 transition-colors"
-                data-color="#ffffff"
-              ></button>
-              <button
-                class="size-8 rounded-full bg-red-500 border-2 border-slate-300 hover:border-slate-400 transition-colors"
-                data-color="#ef4444"
-              ></button>
-              <button
-                class="size-8 rounded-full bg-blue-500 border-2 border-slate-300 hover:border-slate-400 transition-colors"
-                data-color="#3b82f6"
-              ></button>
-            </div>
-          </div>
-        </div>
-      </div>
-      <div class="mt-8 bg-white rounded-xl shadow-sm overflow-hidden">
-        <div class="p-4 border-b border-slate-200">
-          <h2 class="text-slate-800 text-xl font-semibold leading-tight tracking-[-0.01em]">
-            Actions
-          </h2>
-        </div>
-        <div class="p-4 space-y-4">
-          <button
-            id="saveButton"
-            class="flex w-full items-center justify-center gap-2 rounded-lg bg-sky-600 hover:bg-sky-700 text-white text-sm font-medium leading-normal tracking-[0.015em] transition-colors h-10 px-4"
-          >
-            <span class="material-icons text-lg">save</span>
-            <span class="truncate">Save Design</span>
-          </button>
-          <button
-            id="playButton"
-            class="flex w-full items-center justify-center gap-2 rounded-lg bg-emerald-500 hover:bg-emerald-600 text-white text-sm font-medium leading-normal tracking-[0.015em] transition-colors h-10 px-4"
-          >
-            <span class="material-icons text-lg">play_arrow</span>
-            <span class="truncate">Play Design</span>
-          </button>
-        </div>
-      </div>
-    </div>
-  </div>
-</div>
-{% endblock %}
-
-{% block scripts %}
-<script src="/static/js/design.js"></script>
-{% endblock %} 

+ 0 - 408
templates/image2sand.html

@@ -1,408 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Image2Sand - {{ app_name or 'Dune Weaver' }}{% endblock %}
-
-{% block additional_styles %}
-<style>
-@keyframes spin {
-  0% { transform: rotate(0deg); }
-  100% { transform: rotate(360deg); }
-}
-
-.spinner {
-  animation: spin 1s linear infinite;
-}
-
-.custom-checkbox::after {
-  content: "✓";
-  color: white;
-  font-size: 0.75rem;
-  font-weight: bold;
-}
-
-/* Override base template's dark mode rules for image2sand page in light mode */
-html:not(.dark) .bg-white {
-  background-color: #ffffff !important;
-}
-
-html:not(.dark) .bg-gray-50 {
-  background-color: #f9fafb !important;
-}
-
-html:not(.dark) .bg-gray-100 {
-  background-color: #f3f4f6 !important;
-}
-
-html:not(.dark) .bg-gray-200 {
-  background-color: #e5e7eb !important;
-}
-
-html:not(.dark) .text-gray-900 {
-  color: #111827 !important;
-}
-
-html:not(.dark) .text-gray-800 {
-  color: #1f2937 !important;
-}
-
-html:not(.dark) .text-gray-700 {
-  color: #374151 !important;
-}
-
-html:not(.dark) .text-gray-600 {
-  color: #4b5563 !important;
-}
-
-html:not(.dark) .text-gray-500 {
-  color: #6b7280 !important;
-}
-
-html:not(.dark) .text-gray-400 {
-  color: #9ca3af !important;
-}
-
-html:not(.dark) .text-gray-300 {
-  color: #d1d5db !important;
-}
-
-html:not(.dark) .border-gray-200 {
-  border-color: #e5e7eb !important;
-}
-
-html:not(.dark) .border-gray-300 {
-  border-color: #d1d5db !important;
-}
-
-/* Fix hover states in light mode */
-html:not(.dark) .hover\:bg-gray-50:hover {
-  background-color: #f9fafb !important;
-}
-
-html:not(.dark) .hover\:bg-gray-100:hover {
-  background-color: #f3f4f6 !important;
-}
-
-html:not(.dark) .hover\:bg-gray-200:hover {
-  background-color: #e5e7eb !important;
-}
-
-html:not(.dark) .hover\:text-gray-700:hover {
-  color: #374151 !important;
-}
-
-html:not(.dark) .hover\:text-gray-800:hover {
-  color: #1f2937 !important;
-}
-
-html:not(.dark) .hover\:text-gray-900:hover {
-  color: #111827 !important;
-}
-
-/* Fix focus states */
-html:not(.dark) .focus\:ring-blue-500:focus {
-  --tw-ring-color: #3b82f6 !important;
-}
-
-html:not(.dark) .focus\:border-blue-500:focus {
-  border-color: #3b82f6 !important;
-}
-</style>
-{% endblock %}
-
-{% block scripts %}
-<!-- OpenCV.js for image processing -->
-<script async src="/static/js/opencv.js" onload="onOpenCvReady();" type="text/javascript"></script>
-<!-- FontAwesome for icons -->
-<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
-<!-- Image2Sand processing scripts -->
-<script src="/static/js/image2sand-init.js"></script>
-<script src="/static/js/image2sand.js"></script>
-{% endblock %}
-
-{% block content %}
-<div class="min-h-screen p-4 sm:p-8 pt-2 pb-[75px]">
-  <div class="max-w-4xl mx-auto bg-white dark:bg-gray-900 rounded-xl sm:rounded-2xl overflow-hidden shadow-lg border border-gray-200 dark:border-gray-700">
-    <div class="p-4 sm:p-8 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700">
-      <h1 class="text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100 m-0">Convert Image to Pattern</h1>
-      <p class="mt-2 text-gray-600 dark:text-gray-400 text-base sm:text-md">Transform your images into kinetic sand table patterns with advanced edge detection and path optimization. Works well with images with less colors, high contrast, and have clearly defined & enclosed areas.</p>
-    </div>
-    
-    <!-- Image Converter Section - Required structure for JS -->
-    <section id="image-converter" class="visible">
-      <div class="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-8 p-4 sm:p-8 min-h-[400px] sm:min-h-[600px] bg-white dark:bg-gray-900 image-converter-content">
-        
-        <!-- Processing Indicator -->
-        <div id="processing-status" class="hidden fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex flex-col items-center gap-4 z-10 bg-white dark:bg-gray-800 p-6 sm:p-8 rounded-lg shadow-2xl border border-gray-200 dark:border-gray-600 processing-indicator">
-          <div class="spinner w-8 h-8 border-4 border-gray-200 border-t-blue-500 rounded-full"></div>
-          <span id="processing-message" class="text-gray-700 dark:text-gray-300">Processing image...</span>
-        </div>
-        
-        <!-- Image Steps (Left Column) -->
-        <div class="lg:col-span-2 flex flex-col bg-gray-50 dark:bg-gray-800 rounded-lg sm:rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden image-converter-steps">
-          <!-- Tab Content -->
-          <div class="flex-1 relative">
-            <div class="tab-content flex items-center justify-center p-4 sm:p-8 min-h-[300px] sm:min-h-[400px] bg-white dark:bg-gray-900" id="original-tab" data-group="image-converter">
-              <div class="upload-placeholder flex flex-col items-center justify-center gap-3 sm:gap-4 text-gray-500 dark:text-gray-400 text-center">
-                <i class="fa-solid fa-image text-4xl sm:text-6xl text-gray-300 dark:text-gray-600"></i>
-                <h3 class="text-lg sm:text-xl font-semibold text-gray-700 dark:text-gray-300">Upload an Image</h3>
-                <p class="text-sm sm:text-base text-gray-500 dark:text-gray-400">Select an image file to begin the conversion process</p>
-                <button class="bg-blue-600 hover:bg-blue-700 text-white px-4 sm:px-6 py-2 sm:py-3 rounded-lg font-semibold transition-colors flex items-center gap-2 shadow-sm text-sm sm:text-base" onclick="document.getElementById('file-input').click()">
-                  <i class="fa-solid fa-upload"></i>
-                  Choose Image
-                </button>
-              </div>
-              <div class="canvas-wrapper w-full h-full flex items-center justify-center overflow-auto" style="max-height:400px;">
-                <canvas id="original-image" class="hidden max-w-full max-h-[400px] rounded-lg shadow-sm"></canvas>
-              </div>
-            </div>
-
-            <div class="tab-content hidden flex items-center justify-center p-4 sm:p-8 min-h-[300px] sm:min-h-[400px] bg-white dark:bg-gray-900" id="edge-tab" data-group="image-converter">
-              <div class="upload-placeholder flex flex-col items-center justify-center gap-3 sm:gap-4 text-gray-500 dark:text-gray-400 text-center">
-                <i class="fa-solid fa-bezier-curve text-4xl sm:text-6xl text-gray-300 dark:text-gray-600"></i>
-                <h3 class="text-lg sm:text-xl font-semibold text-gray-700 dark:text-gray-300">Edge Detection</h3>
-                <p class="text-sm sm:text-base text-gray-500 dark:text-gray-400">Processed edges will appear here after uploading an image</p>
-              </div>
-              <div class="canvas-wrapper w-full h-full flex items-center justify-center overflow-auto" style="max-height:400px;">
-                <canvas id="edge-image" class="hidden max-w-full max-h-[400px] rounded-lg shadow-sm"></canvas>
-              </div>
-            </div>
-
-            <div class="tab-content hidden flex items-center justify-center p-4 sm:p-8 min-h-[300px] sm:min-h-[400px] bg-white dark:bg-gray-900" id="dot-tab" data-group="image-converter">
-              <div class="upload-placeholder flex flex-col items-center justify-center gap-3 sm:gap-4 text-gray-500 dark:text-gray-400 text-center">
-                <i class="fa-solid fa-circle-dot text-4xl sm:text-6xl text-gray-300 dark:text-gray-600"></i>
-                <h3 class="text-lg sm:text-xl font-semibold text-gray-700 dark:text-gray-300">Identified Points</h3>
-                <p class="text-sm sm:text-base text-gray-500 dark:text-gray-400">Key points for the sand table path will be shown here</p>
-              </div>
-              <div class="canvas-wrapper w-full h-full flex items-center justify-center overflow-auto" style="max-height:400px;">
-                <canvas id="dot-image" class="hidden max-w-full max-h-[400px] rounded-lg shadow-sm"></canvas>
-              </div>
-            </div>
-          </div>
-
-          <!-- Tab Navigation -->
-          <div class="flex border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 tab-container">
-            <button class="tab-button flex-1 p-3 sm:p-4 text-xs sm:text-sm font-medium text-white bg-blue-600 border-r border-gray-200 dark:border-gray-700 hover:bg-blue-700 transition-colors" onclick="switchTab('original', 'image-converter')" id="nav-original" data-group="image-converter">Original Image</button>
-            <button class="tab-button flex-1 p-3 sm:p-4 text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-300 border-r border-gray-200 dark:border-gray-700 transition-colors" onclick="switchTab('edge', 'image-converter')" id="nav-edge" data-group="image-converter">Detected Edges</button>
-            <button class="tab-button flex-1 p-3 sm:p-4 text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-300 transition-colors" onclick="switchTab('dot', 'image-converter')" id="nav-dot" data-group="image-converter">Identified Points</button>
-          </div>
-        </div>
-
-        <!-- Settings Column -->
-        <div class="bg-gray-50 dark:bg-gray-800 rounded-lg sm:rounded-xl border border-gray-200 dark:border-gray-700 p-4 sm:p-6 flex flex-col gap-4 sm:gap-6 image-converter-settings">
-          <h3 class="text-base sm:text-lg font-semibold text-gray-900 dark:text-gray-300 m-0">Processing Settings</h3>
-          
-          <div class="flex flex-col gap-2 setting-item">
-            <label for="epsilon-slider" class="font-medium text-gray-700 dark:text-gray-300 text-sm">Detail Level</label>
-            <div class="flex flex-col gap-1 slider-container">
-              <input type="range" id="epsilon-slider" min="0.1" max="1" step="0.05" value="0.3" class="w-full h-2 bg-gray-200 dark:bg-gray-600 rounded-lg appearance-none cursor-pointer">
-              <div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 slider-labels">
-                <small>Fine</small>
-                <small id="epsilon-value-display" class="font-medium">0.3</small>
-                <small>Coarse</small>
-              </div>
-            </div>
-          </div>
-          
-          <div class="flex flex-col gap-2 setting-item">
-            <label for="dot-number" class="font-medium text-gray-700 dark:text-gray-300 text-sm">Point Limit</label>
-            <select id="dot-number" class="p-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
-              <option value="100">100 points</option>
-              <option value="200">200 points</option>
-              <option value="300">300 points</option>
-              <option value="400">400 points</option>
-              <option value="500" selected>500 points</option>
-              <option value="1000">1000 points</option>
-            </select>
-          </div>
-          
-          <div class="flex flex-col gap-2 setting-item">
-            <label for="contour-mode" class="font-medium text-gray-700 dark:text-gray-300 text-sm">Contour Mode</label>
-            <select id="contour-mode" class="p-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
-              <option value="External">External Only</option>
-              <option value="Tree" selected>External + Internal</option>
-            </select>
-          </div>
-          
-          <div class="flex flex-col gap-3 control-group">
-            <label class="custom-input flex items-center gap-2 cursor-pointer text-sm text-gray-700 dark:text-gray-300">
-              <input type="checkbox" id="is-loop" class="hidden">
-              <span class="custom-checkbox w-4 h-4 border-2 border-gray-300 dark:border-gray-600 rounded flex items-center justify-center transition-all"></span>
-              Loop Drawing
-            </label>
-            <label class="custom-input flex items-center gap-2 cursor-pointer text-sm text-gray-700 dark:text-gray-300">
-              <input type="checkbox" id="no-shortcuts" checked class="hidden">
-              <span class="custom-checkbox w-4 h-4 border-2 border-blue-600 bg-blue-600 rounded flex items-center justify-center transition-all"></span>
-              No Shortcuts
-            </label>
-          </div>
-          
-          <input type="hidden" id="output-type" value="2">
-          
-          <div class="mt-auto generate-button-container">
-            <button id="generate-button" class="w-full bg-blue-600 hover:bg-blue-700 text-white p-2 sm:p-3 rounded-lg font-semibold transition-colors flex items-center justify-center gap-2 text-sm sm:text-base cta" onclick="regeneratePattern()">
-              <i class="fa-solid fa-wand-magic-sparkles"></i>
-              <span>Regenerate Pattern</span>
-            </button>
-          </div>
-        </div>
-
-        <!-- Preview Section -->
-        <div class="lg:col-span-3 bg-gray-50 dark:bg-gray-800 rounded-lg sm:rounded-xl border border-gray-200 dark:border-gray-700 p-4 sm:p-6 flex flex-col gap-3 sm:gap-4 dune-weaver-preview">
-          <h3 class="text-base sm:text-lg font-semibold text-gray-900 dark:text-gray-300 m-0">Dune Weaver Preview</h3>
-          <div class="preview-canvas-wrapper flex items-center justify-center w-full" style="max-width:500px; max-height:500px; margin:auto;">
-            <canvas id="connect-image" class="flex-1 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 min-h-[200px] sm:min-h-[300px] max-w-full max-h-[500px]" style="max-width:500px; max-height:500px;"></canvas>
-          </div>
-          <div id="total-points" class="text-sm text-gray-500 dark:text-gray-400"></div>
-          <div style="display: none;">
-            <h4>contours</h4>
-            <canvas id="plotcontours" width="400" height="400" style="border:1px solid #000000;"></canvas>
-            <button id="plotButton">Plot Next Contour</button>
-          </div>
-        </div>
-
-        <!-- Hidden compatibility elements -->
-        <div style="display: none;">
-          <label id="generation-status" style="display: none;">Image is generating - please wait...</label>
-          <textarea id="polar-coordinates-textarea"></textarea>
-          <div id="simple-coords"></div>
-          <div id="simple-coords-title"></div>
-          <input type="checkbox" id="gaussian-blur-toggle">
-          <div>
-            <input type="file" id="file-input" accept="image/*">
-            <button id="file-button"></button>
-            <span id="file-name"></span>
-          </div>
-        </div>
-        
-        <!-- Actions -->
-        <div class="lg:col-span-3 flex justify-end gap-3 sm:gap-4 pt-4 sm:pt-6 border-t border-gray-200 dark:border-gray-700 image-converter-actions">
-          <button class="secondary bg-white hover:bg-gray-50 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 px-4 sm:px-6 py-2 rounded-lg font-medium transition-colors flex items-center gap-2 border border-gray-300 dark:border-gray-600 text-sm sm:text-base cancel" onclick="window.history.back()">
-            <i class="fa-solid fa-arrow-left"></i>
-            <span>Back</span>
-          </button>
-          <button class="cta bg-blue-600 hover:bg-blue-700 text-white px-4 sm:px-6 py-2 rounded-lg font-medium transition-colors flex items-center gap-2 text-sm sm:text-base" id="save-pattern-button" onclick="savePattern()">
-            <i class="fa-solid fa-floppy-disk"></i>
-            <span>Save Pattern</span>
-          </button>
-        </div>
-      </div>
-    </section>
-  </div>
-</div>
-
-<script>
-// OpenCV ready callback
-function onOpenCvReady() {
-  console.log('OpenCV.js is ready');
-}
-
-// Tab switching function
-function switchTab(tabName, group) {
-  // Hide all tab contents
-  document.querySelectorAll(`[data-group="${group}"].tab-content`).forEach(tab => {
-    tab.classList.add('hidden');
-    tab.style.display = 'none';
-    
-    // Hide all canvases in the tab
-    const canvas = tab.querySelector('canvas');
-    if (canvas) {
-      canvas.classList.add('hidden');
-    }
-    
-    // Show the placeholder
-    const placeholder = tab.querySelector('.upload-placeholder');
-    if (placeholder) {
-      placeholder.style.display = 'flex';
-    }
-  });
-  
-  // Remove active styling from all tab buttons
-  document.querySelectorAll(`[data-group="${group}"].tab-button`).forEach(btn => {
-    btn.className = 'tab-button flex-1 p-3 sm:p-4 text-xs sm:text-sm font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-800 dark:hover:text-gray-300 border-r border-gray-200 dark:border-gray-700 transition-colors';
-  });
-  
-  // Show selected tab content
-  const selectedTab = document.getElementById(`${tabName}-tab`);
-  selectedTab.classList.remove('hidden');
-  selectedTab.style.display = 'flex';
-  
-  // Show the canvas if it exists and has content
-  const canvas = selectedTab.querySelector('canvas');
-  if (canvas && canvas.width > 0 && canvas.height > 0) {
-    canvas.classList.remove('hidden');
-    // Hide the placeholder if canvas has content
-    const placeholder = selectedTab.querySelector('.upload-placeholder');
-    if (placeholder) {
-      placeholder.style.display = 'none';
-    }
-  }
-  
-  // Add active styling to selected tab button
-  const activeButton = document.getElementById(`nav-${tabName}`);
-  activeButton.className = 'tab-button flex-1 p-3 sm:p-4 text-xs sm:text-sm font-medium text-white bg-blue-600 border-r border-gray-200 dark:border-gray-700 hover:bg-blue-700 transition-colors';
-  
-  // Remove border-r from last button
-  if (activeButton.nextElementSibling === null) {
-    activeButton.className = activeButton.className.replace('border-r border-gray-200 dark:border-gray-700', '');
-  }
-}
-
-document.addEventListener('DOMContentLoaded', function() {
-  const epsilonSlider = document.getElementById('epsilon-slider');
-  const epsilonDisplay = document.getElementById('epsilon-value-display');
-  const fileInput = document.getElementById('file-input');
-
-  // Epsilon slider update
-  epsilonSlider.addEventListener('input', function() {
-    epsilonDisplay.textContent = this.value;
-  });
-
-  // File input handling
-  fileInput.addEventListener('change', function(event) {
-    if (event.target.files.length > 0) {
-      handleImageUpload(event);
-    }
-  });
-
-  // Custom checkbox handling
-  document.querySelectorAll('.custom-input').forEach(label => {
-    const checkbox = label.querySelector('input[type="checkbox"]');
-    const customCheckbox = label.querySelector('.custom-checkbox');
-    
-    label.addEventListener('click', function(e) {
-      e.preventDefault();
-      checkbox.checked = !checkbox.checked;
-      updateCheckboxAppearance(checkbox, customCheckbox);
-    });
-    
-    // Initialize appearance
-    updateCheckboxAppearance(checkbox, customCheckbox);
-  });
-  
-  function updateCheckboxAppearance(checkbox, customCheckbox) {
-    if (checkbox.checked) {
-      customCheckbox.className = 'custom-checkbox w-4 h-4 border-2 border-blue-600 bg-blue-600 rounded flex items-center justify-center transition-all';
-    } else {
-      customCheckbox.className = 'custom-checkbox w-4 h-4 border-2 border-gray-300 dark:border-gray-600 rounded flex items-center justify-center transition-all';
-    }
-  }
-});
-
-// Regenerate pattern function
-function regeneratePattern() {
-  convertImage();
-}
-
-// Save pattern function
-async function savePattern() {
-  try {
-    await saveConvertedPattern();
-  } catch (error) {
-    console.error('Error saving pattern:', error);
-    if (typeof showStatusMessage === 'function') {
-      showStatusMessage('Error saving pattern: ' + error.message, 'error');
-    }
-  }
-}
-</script>
-{% endblock %} 

+ 0 - 654
templates/index.html

@@ -1,654 +0,0 @@
-{% extends "base.html" %}
-
-{% block additional_styles %}
-.pattern-card {
-  transition: transform 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
-  cursor: pointer;
-}
-.pattern-card:hover {
-  transform: translateY(-4px);
-  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
-    0 4px 6px -2px rgba(0, 0, 0, 0.05);
-}
-.pattern-card.selected {
-  border: 2px solid #0c7ff2;
-}
-
-/* Pattern grid - responsive columns */
-#patternGrid {
-  grid-template-columns: repeat(3, 1fr);
-  justify-content: center;
-}
-@media (min-width: 480px) {
-  #patternGrid {
-    grid-template-columns: repeat(auto-fill, minmax(100px, 128px));
-  }
-}
-
-@media (min-width: 1024px) {
-  .preview-open .layout-content-container {
-    margin-right: 28rem;
-  }
-  #patternPreviewPanel {
-    transform: translateX(0);
-  }
-}
-.delete-button-disabled {
-  opacity: 0.5;
-  cursor: not-allowed;
-  pointer-events: none;
-  background-color: #f3f4f6 !important;
-  border-color: #d1d5db !important;
-  color: #9ca3af !important;
-}
-
-/* Pattern preview styles */
-.pattern-preview {
-  overflow: hidden;
-  aspect-ratio: 1;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  border: 1px solid #e5e7eb;
-}
-.pattern-preview img {
-  width: 100%;
-  height: 100%;
-  object-fit: contain;
-}
-
-/* Pattern preview panel styles */
-#patternPreviewPanel .bg-slate-200 {
-  background-color: #f3f4f6;
-  border: 1px solid #e5e7eb;
-  border-radius: 9999px;
-  overflow: hidden;
-}
-#patternPreviewPanel img {
-  width: 100%;
-  height: 100%;
-  object-fit: contain;
-}
-
-/* Dark mode styles for index page */
-.dark .pattern-card {
-  background-color: #262626;
-  border-color: #404040;
-}
-.dark .pattern-card:hover {
-  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3),
-    0 4px 6px -2px rgba(0, 0, 0, 0.2);
-}
-.dark .pattern-card.selected {
-  border-color: #0c7ff2;
-}
-.dark .delete-button-disabled {
-  background-color: #262626 !important;
-  border-color: #404040 !important;
-  color: #6b7280 !important;
-}
-.dark #patternPreviewPanel {
-  background-color: #262626;
-}
-.dark #patternPreviewPanel header {
-  border-color: #404040;
-}
-.dark #patternPreviewPanel h2 {
-  color: #e5e5e5;
-}
-.dark #patternPreviewPanel .text-slate-500 {
-  color: #9ca3af;
-}
-.dark #patternPreviewPanel .text-slate-700 {
-  color: #d1d5db;
-}
-.dark #patternPreviewPanel .border-slate-300 {
-  border-color: #404040;
-}
-.dark #patternPreviewPanel .bg-slate-200 {
-  background-color: #262626;
-  border-color: #404040;
-}
-.dark #patternPreviewPanel .hover\:bg-slate-100:hover {
-  background-color: #404040;
-}
-.dark #patternPreviewPanel .hover\:text-slate-700:hover {
-  color: #e5e5e5;
-}
-.dark .form-input {
-  background-color: #262626;
-  border-color: #404040;
-  color: #e5e5e5;
-}
-.dark .form-input::placeholder {
-  color: #9ca3af;
-}
-.dark .form-input:focus {
-  border-color: #0c7ff2;
-  ring-color: #0c7ff2;
-}
-.dark .bg-gray-50 {
-  background-color: #1a1a1a;
-}
-.dark .bg-white {
-  background-color: #262626;
-}
-.dark .text-gray-900 {
-  color: #e5e5e5;
-}
-.dark .text-gray-500 {
-  color: #9ca3af;
-}
-.dark .text-gray-400 {
-  color: #9ca3af;
-}
-.dark .border-gray-300 {
-  border-color: #404040;
-}
-.dark .hover\:bg-red-50:hover {
-  background-color: #2d1a1a;
-}
-.dark .focus\:ring-offset-2 {
-  --tw-ring-offset-color: #262626;
-}
-.dark .text-\[\#111518\] {
-  color: #e5e5e5;
-}
-.dark .border-b-\[\#f0f2f5\] {
-  border-color: #404040;
-}
-.dark .hover\:bg-slate-100:hover {
-  background-color: #404040;
-}
-.dark .hover\:text-slate-700:hover {
-  color: #e5e5e5;
-}
-.dark .has-\[\:checked\]\:bg-\[\#0b80ee\]:has(:checked) {
-  background-color: #0c7ff2;
-}
-.dark .has-\[\:checked\]\:border-\[\#0b80ee\]:has(:checked) {
-  border-color: #0c7ff2;
-}
-.dark .has-\[\:checked\]\:ring-\[\#0b80ee\]:has(:checked) {
-  --tw-ring-color: #0c7ff2;
-}
-.dark .has-\[\:checked\]\:ring-offset-2:has(:checked) {
-  --tw-ring-offset-color: #262626;
-}
-
-/* Dark mode pattern preview styles */
-.dark .pattern-preview {
-  border-color: #404040;
-}
-.dark .pattern-preview img {
-  filter: invert(1);
-}
-.dark #patternPreviewPanel img {
-  filter: invert(1);
-}
-.dark .pattern-card img {
-  filter: invert(1);
-  background-color:rgb(229, 229, 229);
-}
-
-{% endblock %}
-
-{% block content %}
-<div class="layout-content-container flex flex-col w-full max-w-5xl mb-24">
-    <div id="browseHeader" class="flex-none bg-gray-50 py-4">
-        <div class="mt-2 sm:mt-8 lg:mt-2">
-            <!-- Header Row -->
-            <div class="flex items-center justify-between gap-2 mb-4">
-                <h2 class="text-gray-900 text-3xl font-bold leading-tight tracking-tight">
-                    Browse Patterns
-                </h2>
-                <button
-                        id="cacheAllButton"
-                        class="inline-flex gap-1 sm:gap-2 transition-colors items-center justify-center rounded-lg px-3 sm:px-4 py-2 sm:py-2.5 text-xs sm:text-sm font-semibold text-gray-900 hover:text-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 shrink-0"
-                >
-                    <span class="material-icons text-sm pr-1 sm:pr-2">cached</span>
-                    <span class="hidden sm:inline">Cache All Previews</span>
-                    <span class="sm:hidden">Cache</span>
-                </button>
-            </div>
-            
-            <!-- Controls Row -->
-            <div class="flex gap-1 sm:gap-2 items-center text-xs sm:text-sm min-w-0 overflow-x-auto mb-4">
-                <div class="flex items-center gap-1">
-                    <span class="text-xs font-medium text-gray-700 hidden sm:inline">Sort:</span>
-                    <select id="browseSortFieldSelect" class="text-xs rounded border border-gray-300 bg-white text-gray-900 px-1 sm:px-2 py-1 min-w-0 flex-shrink" disabled>
-                        <option value="name">Name</option>
-                        <option value="date">Date Modified</option>
-                        <option value="coordinates">Coordinates</option>
-                        <option value="favorite">Favorite</option>
-                    </select>
-                    <button id="browseSortDirectionBtn" class="p-1 rounded hover:bg-gray-200 text-gray-500 opacity-50 cursor-not-allowed" title="Loading..." disabled>
-                        <span class="material-icons text-sm" id="browseSortDirectionIcon">arrow_upward</span>
-                    </button>
-                </div>
-                
-                <div class="flex items-center gap-1 min-w-0">
-                    <span class="text-xs font-medium text-gray-700 hidden sm:inline">Folder:</span>
-                    <select id="browseCategoryFilterSelect" class="text-xs rounded border border-gray-300 bg-white text-gray-900 px-1 sm:px-2 py-1 min-w-0 flex-shrink" disabled>
-                        <option value="all">All Folders (loading...)</option>
-                    </select>
-                </div>
-            </div>
-        </div>
-
-        <div class="relative w-full sm:w-auto flex grow gap-2 items-center flex-wrap">
-            <div class="flex gap-2 flex-1">
-                <div class="relative flex-1">
-                    <input
-                            id="patternSearch"
-                            class="form-input block w-full h-11 rounded-lg border-gray-300 bg-gray-50 py-2.5 p-4 text-gray-900 placeholder-gray-500 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 sm:text-sm"
-                            placeholder="Search patterns..."
-                            type="search"
-                    />
-                    <button
-                            id="searchButton"
-                            class="inline-flex absolute top-0 right-0 h-11 w-11 items-center justify-center rounded-lg bg-blue-600 p-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
-                    >
-                        <span class="material-icons text-sm font-semibold text-white">search</span>
-                    </button>
-                </div>
-            </div>
-            <!-- Add Pattern Button -->
-            <input
-                    type="file"
-                    id="patternFileInput"
-                    accept=".thr"
-                    multiple
-                    class="hidden"
-            />
-            <button
-                    aria-label="Add new pattern"
-                    class="inline-flex items-center justify-center rounded-lg h-11 w-11 bg-blue-600 text-white shadow-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ml-2 text-xl font-bold"
-                    onclick="document.getElementById('patternFileInput').click()"
-            >
-                <span class="material-icons">add</span>
-            </button>
-        </div>
-    </div>
-    <section class="px-4 py-6">
-        <div id="patternGrid" class="grid gap-2 sm:gap-4 md:gap-6">
-            <!-- All patterns will be populated here -->
-        </div>
-        <!-- Spacer to allow scrolling past last patterns -->
-        <div style="height: 200px; background: transparent;"></div>
-    </section>
-</div>
-
-<!-- Pattern Preview Panel -->
-<div id="patternPreviewPanel"
-     class="fixed top-0 bottom-0 right-0 w-full max-w-md transform translate-x-full transition-transform duration-300 ease-in-out z-40 lg:translate-x-0 lg:opacity-0 lg:pointer-events-none">
-    <div class="h-full bg-white shadow-xl">
-        <header class="flex items-center justify-between border-b border-solid border-b-[#f0f2f5] px-6 py-3 sm:py-4">
-            <div class="flex items-center gap-3 text-[#111518]">
-                <h2 id="patternPreviewTitle" class="text-[#111518] text-lg font-semibold leading-tight">Pattern
-                    Details</h2>
-            </div>
-            <button id="closePreviewPanel"
-                    class="flex cursor-pointer items-center justify-center rounded-full p-1.5 text-slate-500 hover:bg-slate-100 hover:text-slate-700 focus:outline-none focus:ring-2 focus:ring-[#0b80ee]">
-                <span class="material-icons text-2xl">close</span>
-            </button>
-        </header>
-        <div class="p-6 overflow-y-auto h-[calc(100%-4rem)]">
-            <div class="mb-6 aspect-square w-full overflow-hidden pattern-preview rounded-full relative group">
-                <img id="patternPreviewImage" alt="Pattern Preview" class="h-full w-full object-cover" src=""/>
-                <!-- Play button overlay for animated preview -->
-                <div id="previewPlayOverlay"
-                     class="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200 cursor-pointer">
-                    <div class="bg-white rounded-full p-3 shadow-lg flex items-center justify-center w-12 h-12">
-                        <span class="material-icons text-2xl text-gray-800">play_arrow</span>
-                    </div>
-                </div>
-            </div>
-            <div class="mb-4 flex justify-between text-sm">
-                <div class="flex items-center gap-2">
-                    <span class="text-slate-500">First:</span>
-                    <span id="firstCoordinate" class="font-semibold text-[#111518]">(0, 0)</span>
-                </div>
-                <div class="flex items-center gap-2">
-                    <span class="text-slate-500">Last:</span>
-                    <span id="lastCoordinate" class="font-semibold text-[#111518]">(0, 0)</span>
-                </div>
-            </div>
-            <div class="mb-4">
-                <h3 class="mb-2 text-sm font-semibold text-slate-700">Pre-Execution Action</h3>
-                <div class="grid grid-cols-2 gap-2">
-                    <label class="group relative flex cursor-pointer items-center justify-center rounded-lg border border-slate-300 p-2 text-center text-sm font-medium text-slate-700 transition-all hover:border-[#0b80ee] has-[:checked]:border-[#0b80ee] has-[:checked]:bg-[#0b80ee] has-[:checked]:text-white has-[:checked]:ring-2 has-[:checked]:ring-[#0b80ee] has-[:checked]:ring-offset-2">
-                        Adaptive
-                        <input checked="" class="peer sr-only" name="preExecutionAction" type="radio" value="adaptive"/>
-                    </label>
-                    <label class="group relative flex cursor-pointer items-center justify-center rounded-lg border border-slate-300 p-2 text-center text-sm font-medium text-slate-700 transition-all hover:border-[#0b80ee] has-[:checked]:border-[#0b80ee] has-[:checked]:bg-[#0b80ee] has-[:checked]:text-white has-[:checked]:ring-2 has-[:checked]:ring-[#0b80ee] has-[:checked]:ring-offset-2">
-                        Clear From Center
-                        <input class="peer sr-only" name="preExecutionAction" type="radio" value="clear_from_in"/>
-                    </label>
-                    <label class="group relative flex cursor-pointer items-center justify-center rounded-lg border border-slate-300 p-2 text-center text-sm font-medium text-slate-700 transition-all hover:border-[#0b80ee] has-[:checked]:border-[#0b80ee] has-[:checked]:bg-[#0b80ee] has-[:checked]:text-white has-[:checked]:ring-2 has-[:checked]:ring-[#0b80ee] has-[:checked]:ring-offset-2">
-                        Clear From Perimeter
-                        <input class="peer sr-only" name="preExecutionAction" type="radio" value="clear_from_out"/>
-                    </label>
-                    <label class="group relative flex cursor-pointer items-center justify-center rounded-lg border border-slate-300 p-2 text-center text-sm font-medium text-slate-700 transition-all hover:border-[#0b80ee] has-[:checked]:border-[#0b80ee] has-[:checked]:bg-[#0b80ee] has-[:checked]:text-white has-[:checked]:ring-2 has-[:checked]:ring-[#0b80ee] has-[:checked]:ring-offset-2">
-                        Clear Sideway
-                        <input class="peer sr-only" name="preExecutionAction" type="radio" value="clear_sideway"/>
-                    </label>
-                    <label class="group relative flex cursor-pointer items-center justify-center rounded-lg border border-slate-300 p-2 text-center text-sm font-medium text-slate-700 transition-all hover:border-[#0b80ee] has-[:checked]:border-[#0b80ee] has-[:checked]:bg-[#0b80ee] has-[:checked]:text-white has-[:checked]:ring-2 has-[:checked]:ring-[#0b80ee] has-[:checked]:ring-offset-2">
-                        None
-                        <input class="peer sr-only" name="preExecutionAction" type="radio" value="none"/>
-                    </label>
-                </div>
-            </div>
-            <div class="space-y-3">
-                <button id="playPattern"
-                        class="flex w-full items-center justify-center gap-2 rounded-lg bg-[#0b80ee] px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-opacity-90 focus:outline-none focus:ring-2 focus:ring-[#0b80ee] focus:ring-offset-2">
-                    <span class="material-icons text-lg">play_arrow</span>
-                    Play
-                </button>
-                <button id="deletePattern"
-                        class="flex w-full items-center justify-center gap-2 rounded-lg border border-red-500 px-4 py-2.5 text-sm font-semibold text-red-500 shadow-sm transition-colors hover:bg-red-50 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2">
-                    <span class="material-icons text-lg">delete</span>
-                    Delete
-                </button>
-            </div>
-        </div>
-    </div>
-</div>
-
-<!-- Animated Preview Modal -->
-<div id="animatedPreviewModal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden">
-    <div class="flex items-center justify-center min-h-screen p-4">
-        <div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[95vh] flex flex-col overflow-hidden">
-            <!-- Modal Header -->
-            <div class="flex items-center justify-between p-6 border-b border-gray-200 shrink-0">
-                <h3 id="animatedPreviewTitle" class="text-xl font-semibold text-gray-900">Animated Preview</h3>
-                <button id="closeAnimatedPreview" class="text-gray-400 hover:text-gray-600">
-                    <span class="material-icons text-2xl">close</span>
-                </button>
-            </div>
-
-            <!-- Modal Content -->
-            <div class="p-6 overflow-y-auto grow h-full">
-                <!-- Canvas Container -->
-                <div class="relative flex justify-center h-full">
-                    <canvas id="animatedPreviewCanvas"
-                            class="border border-gray-300 rounded-full dark:invert"></canvas>
-                    <!-- Play/Pause Overlay -->
-                    <div id="playPauseOverlay"
-                         class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-20 opacity-0 transition-opacity duration-200 cursor-pointer rounded-full">
-                        <div class="bg-white rounded-full p-4 shadow-lg">
-                            <span id="playPauseIcon" class="material-icons text-3xl text-gray-800">play_arrow</span>
-                        </div>
-                    </div>
-                </div>
-            </div>
-
-            <!-- Controls -->
-            <div class="p-6 space-y-4 shrink-0">
-                <!-- Speed Control -->
-                <div>
-                    <label class="block text-sm font-medium text-gray-700 mb-2">Speed</label>
-                    <div class="flex items-center gap-4">
-                        <input type="range" id="speedSlider" min="0.1" max="5" step="0.1" value="1" class="flex-1">
-                        <span id="speedValue" class="text-sm font-medium text-gray-900 w-16">1x</span>
-                    </div>
-                </div>
-
-                <!-- Progress Control -->
-                <div>
-                    <label class="block text-sm font-medium text-gray-700 mb-2">Progress</label>
-                    <div class="flex items-center gap-4">
-                        <input type="range" id="progressSlider" min="0" max="100" step="0.1" value="0"
-                               class="flex-1">
-                        <span id="progressValue" class="text-sm font-medium text-gray-900 w-20">0%</span>
-                    </div>
-                </div>
-
-                <!-- Control Buttons -->
-                <div class="flex items-center justify-center gap-4">
-                    <button id="playPauseBtn"
-                            class="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
-                        <span id="playPauseBtnIcon" class="material-icons">play_arrow</span>
-                        <span id="playPauseBtnText">Play</span>
-                    </button>
-                    <button id="resetBtn"
-                            class="flex items-center gap-2 px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700">
-                        <span class="material-icons">replay</span>
-                        Reset
-                    </button>
-                </div>
-            </div>
-        </div>
-    </div>
-</div>
-{% endblock %}
-
-{% block scripts %}
-<script src="/static/js/index.js"></script>
-<script>
-// File upload is handled in index.js
-
-// Update delete button state when pattern is selected
-function updateDeleteButtonState(patternName) {
-  const deleteButton = document.getElementById('deletePattern');
-  if (!deleteButton) return;
-
-  if (patternName && patternName.startsWith('custom_patterns/')) {
-    deleteButton.classList.remove('delete-button-disabled');
-    deleteButton.disabled = false;
-  } else {
-    deleteButton.classList.add('delete-button-disabled');
-    deleteButton.disabled = true;
-  }
-}
-
-// Override the selectPattern function to update delete button state
-const originalSelectPattern = window.selectPattern;
-window.selectPattern = function(pattern, card) {
-  originalSelectPattern(pattern, card);
-  updateDeleteButtonState(pattern);
-};
-
-// Initialize delete button state when the page loads
-document.addEventListener('DOMContentLoaded', function() {
-  // Initially disable the delete button
-  const deleteButton = document.getElementById('deletePattern');
-  if (deleteButton) {
-    deleteButton.classList.add('delete-button-disabled');
-    deleteButton.disabled = true;
-  }
-});
-
-// Override the loadPatterns function to update delete button state after loading
-const originalLoadPatterns = window.loadPatterns;
-window.loadPatterns = async function() {
-  await originalLoadPatterns();
-  // Update delete button state based on currently selected pattern
-  const selectedPattern = document.querySelector('.pattern-card.selected');
-  if (selectedPattern) {
-    updateDeleteButtonState(selectedPattern.dataset.pattern);
-  } else {
-    updateDeleteButtonState(null);
-  }
-};
-
-// Sticky search bar on desktop
-(function() {
-  // Only apply on large screens (lg: 1024px+)
-  const mediaQuery = window.matchMedia('(min-width: 1024px)');
-  if (!mediaQuery.matches) return;
-
-  // Create a compact fixed search bar
-  const siteHeaderHeight = 56;
-  let fixedBar = null;
-  let isFixed = false;
-  const triggerPoint = 150; // Scroll distance to trigger
-
-  function createFixedBar() {
-    if (fixedBar) return;
-
-    fixedBar = document.createElement('div');
-    fixedBar.id = 'fixedSearchBar';
-    // Get actual header height
-    const siteHeader = document.querySelector('header');
-    const headerHeight = siteHeader ? siteHeader.offsetHeight : 64;
-
-    fixedBar.style.cssText = `
-      position: fixed;
-      top: ${headerHeight}px;
-      left: 0;
-      right: 0;
-      background: #f9fafb;
-      border-bottom: 1px solid #e5e7eb;
-      box-shadow: 0 2px 4px rgba(0,0,0,0.08);
-      z-index: 30;
-      transform: translateY(-100%);
-      transition: transform 0.2s ease;
-    `;
-    if (document.documentElement.classList.contains('dark')) {
-      fixedBar.style.background = '#1a1a1a';
-      fixedBar.style.borderColor = '#404040';
-    }
-
-    fixedBar.innerHTML = `
-      <div class="max-w-5xl mx-auto px-4 py-2 flex items-center gap-3">
-        <div class="flex items-center gap-1 shrink-0">
-          <span class="text-xs font-medium text-gray-700 dark:text-gray-300">Sort:</span>
-          <select id="fixedSortSelect" class="text-xs rounded border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 bg-white text-gray-900 px-2 py-1">
-            <option value="name">Name</option>
-            <option value="date">Date Modified</option>
-            <option value="coordinates">Coordinates</option>
-            <option value="favorite">Favorite</option>
-          </select>
-          <button id="fixedSortDirBtn" class="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700 text-gray-500">
-            <span class="material-icons text-sm" id="fixedSortDirIcon">arrow_upward</span>
-          </button>
-        </div>
-        <div class="relative flex-1">
-          <input id="fixedSearchInput" class="form-input block w-full h-11 rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 bg-gray-50 py-2.5 p-4 text-gray-900 placeholder-gray-500 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 sm:text-sm" placeholder="Search patterns..." type="search" />
-          <button id="fixedSearchBtn" class="inline-flex absolute top-0 right-0 h-11 w-11 items-center justify-center rounded-lg bg-blue-600 p-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
-            <span class="material-icons text-sm font-semibold text-white">search</span>
-          </button>
-        </div>
-        <button id="fixedAddBtn" class="inline-flex items-center justify-center rounded-lg h-11 w-11 bg-blue-600 text-white shadow-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
-          <span class="material-icons">add</span>
-        </button>
-      </div>
-    `;
-
-    document.body.appendChild(fixedBar);
-
-    // Sync with main controls
-    const mainSearch = document.getElementById('patternSearch');
-    const fixedSearch = fixedBar.querySelector('#fixedSearchInput');
-    const fixedSearchBtn = fixedBar.querySelector('#fixedSearchBtn');
-    const fixedAdd = fixedBar.querySelector('#fixedAddBtn');
-
-    // Search on Enter key
-    fixedSearch.addEventListener('keypress', (e) => {
-      if (e.key === 'Enter') {
-        if (mainSearch) mainSearch.value = fixedSearch.value;
-        if (typeof searchPatterns === 'function') {
-          searchPatterns(fixedSearch.value.trim());
-        }
-      }
-    });
-
-    // Also sync on input for live typing
-    fixedSearch.addEventListener('input', () => {
-      if (mainSearch) mainSearch.value = fixedSearch.value;
-    });
-
-    // Search button click
-    fixedSearchBtn.addEventListener('click', () => {
-      if (mainSearch) mainSearch.value = fixedSearch.value;
-      if (typeof searchPatterns === 'function') {
-        searchPatterns(fixedSearch.value.trim());
-      }
-    });
-
-    fixedAdd.addEventListener('click', () => {
-      document.getElementById('patternFileInput')?.click();
-    });
-
-    // Sort controls sync
-    const mainSortSelect = document.getElementById('browseSortFieldSelect');
-    const mainSortDirBtn = document.getElementById('browseSortDirectionBtn');
-    const mainSortDirIcon = document.getElementById('browseSortDirectionIcon');
-    const fixedSortSelect = fixedBar.querySelector('#fixedSortSelect');
-    const fixedSortDirBtn = fixedBar.querySelector('#fixedSortDirBtn');
-    const fixedSortDirIcon = fixedBar.querySelector('#fixedSortDirIcon');
-
-    // Sync initial values from main controls
-    if (mainSortSelect) {
-      fixedSortSelect.value = mainSortSelect.value;
-    }
-    if (mainSortDirIcon) {
-      fixedSortDirIcon.textContent = mainSortDirIcon.textContent;
-    }
-
-    // Fixed sort select change -> sync to main and trigger sort
-    fixedSortSelect.addEventListener('change', () => {
-      if (mainSortSelect) {
-        mainSortSelect.value = fixedSortSelect.value;
-        mainSortSelect.dispatchEvent(new Event('change'));
-      }
-    });
-
-    // Fixed sort direction button click -> sync to main and trigger sort
-    fixedSortDirBtn.addEventListener('click', () => {
-      if (mainSortDirBtn) {
-        mainSortDirBtn.click();
-        // After click, sync the icon
-        setTimeout(() => {
-          if (mainSortDirIcon) {
-            fixedSortDirIcon.textContent = mainSortDirIcon.textContent;
-          }
-        }, 50);
-      }
-    });
-
-    // Listen for changes on main controls to sync back to fixed
-    if (mainSortSelect) {
-      mainSortSelect.addEventListener('change', () => {
-        fixedSortSelect.value = mainSortSelect.value;
-      });
-    }
-
-    // Create observer to sync sort direction icon changes
-    if (mainSortDirIcon) {
-      const observer = new MutationObserver(() => {
-        fixedSortDirIcon.textContent = mainSortDirIcon.textContent;
-      });
-      observer.observe(mainSortDirIcon, { childList: true, characterData: true, subtree: true });
-    }
-  }
-
-  function handleScroll() {
-    if (!mediaQuery.matches) return;
-
-    if (window.scrollY > triggerPoint && !isFixed) {
-      createFixedBar();
-      requestAnimationFrame(() => {
-        fixedBar.style.transform = 'translateY(0)';
-      });
-      isFixed = true;
-    } else if (window.scrollY <= triggerPoint && isFixed) {
-      if (fixedBar) {
-        fixedBar.style.transform = 'translateY(-100%)';
-      }
-      isFixed = false;
-    }
-  }
-
-  window.addEventListener('scroll', handleScroll, { passive: true });
-  mediaQuery.addEventListener('change', (e) => {
-    if (!e.matches && fixedBar) {
-      fixedBar.remove();
-      fixedBar = null;
-      isFixed = false;
-    }
-  });
-})();
-</script>
-{% endblock %}

+ 0 - 387
templates/led.html

@@ -1,387 +0,0 @@
-{% extends "base.html" %} {% block title %}LED Control - {{ app_name or 'Dune Weaver' }}{% endblock %}
-
-{% block additional_head %}
-<link rel="stylesheet" href="/static/css/coloris.min.css">
-{% endblock %}
-
-{% block additional_styles %}
-/* Dark mode styles for LED page */
-.dark .bg-white {
-  background-color: #262626;
-}
-.dark .bg-gray-100 {
-  background-color: #1f1f1f;
-}
-.dark .border-slate-200 {
-  border-color: #404040;
-}
-.dark .border-slate-300 {
-  border-color: #404040;
-}
-.dark .text-gray-500 {
-  color: #9ca3af;
-}
-.dark .text-gray-700 {
-  color: #d1d5db;
-}
-.dark .text-slate-500 {
-  color: #e2e8f0;
-}
-.dark .text-slate-600 {
-  color: #f1f5f9;
-}
-.dark .text-slate-700 {
-  color: #f8fafc;
-}
-.dark .text-slate-800 {
-  color: #ffffff;
-}
-.dark .text-slate-900 {
-  color: #ffffff;
-}
-.dark .bg-slate-50 {
-  background-color: #262626;
-}
-
-/* Specific label overrides for better visibility */
-.dark label {
-  color: #f1f5f9;
-}
-
-/* Form elements */
-.dark input[type="range"] {
-  background-color: #404040;
-}
-.dark input[type="number"],
-.dark input[type="text"]:not(.effect-color-picker) {
-  background-color: #1f1f1f;
-  border-color: #404040;
-  color: #e5e5e5;
-}
-.dark select,
-.dark .form-select {
-  background-color: #1f1f1f;
-  border-color: #404040;
-  color: #e5e5e5;
-}
-.dark select option {
-  background-color: #262626;
-  color: #e5e5e5;
-}
-
-/* Status messages - keep backgrounds but adjust borders */
-.dark .bg-green-50 {
-  background-color: #14532d;
-}
-.dark .border-green-200 {
-  border-color: #166534;
-}
-.dark .text-green-700 {
-  color: #86efac;
-}
-.dark .bg-red-50 {
-  background-color: #450a0a;
-}
-.dark .border-red-200 {
-  border-color: #991b1b;
-}
-.dark .text-red-700 {
-  color: #fca5a5;
-}
-.dark .bg-amber-50 {
-  background-color: #451a03;
-}
-.dark .border-amber-200 {
-  border-color: #92400e;
-}
-.dark .text-amber-700 {
-  color: #fcd34d;
-}
-.dark .bg-blue-50 {
-  background-color: #1f1f1f;
-}
-.dark .border-blue-200 {
-  border-color: #404040;
-}
-.dark .text-blue-700 {
-  color: #e2e8f0;
-}
-.dark .text-blue-800 {
-  color: #f1f5f9;
-}
-
-/* Iframe border */
-.dark iframe {
-  border-color: #404040;
-}
-
-/* Hide hex input in Coloris picker */
-.clr-field input {
-  display: none !important;
-}
-.clr-field button {
-  display: none !important;
-}
-
-/* Hide text inside the circular color pickers */
-.effect-color-picker {
-  color: transparent !important;
-  text-indent: -9999px;
-  font-size: 0;
-  caret-color: transparent;
-}
-{% endblock %}
-
-{% block content %}
-<div class="layout-content-container flex flex-col w-full max-w-4xl gap-0 pt-2 pb-[75px]">
-  <!-- Not Configured State -->
-  <section id="led-not-configured" class="bg-white rounded-xl shadow-sm overflow-hidden pt-4 sm:pt-0 h-full hidden">
-    <div class="flex flex-col items-center px-0 py-0 h-full">
-      <div class="w-full h-full max-w-5xl flex flex-col overflow-hidden">
-        <div class="w-full p-8 text-center">
-          <div class="flex flex-col items-center gap-4">
-            <span class="material-icons text-6xl text-gray-500">lightbulb</span>
-            <h2 class="text-2xl font-semibold text-gray-700">LED Controller Not Configured</h2>
-            <p class="text-gray-500 max-w-md">Please configure your LED controller (WLED or DW LEDs) in the Settings page.</p>
-            <a href="/settings" class="mt-4 flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-3 text-sm font-semibold text-white shadow-md hover:bg-blue-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2">
-              <span class="material-icons">settings</span>
-              Go to Settings
-            </a>
-          </div>
-        </div>
-      </div>
-    </div>
-  </section>
-
-  <!-- WLED iframe -->
-  <section id="wled-container" class="bg-white rounded-xl shadow-sm overflow-hidden pt-4 sm:pt-0 h-full hidden">
-    <div class="flex flex-col items-center px-0 py-0 h-full">
-      <div class="w-full h-full max-w-5xl flex flex-col overflow-hidden">
-        <iframe id="wled-frame"
-          src=""
-          class="h-full w-full rounded-lg border border-slate-200"
-          frameborder="0"
-          allowfullscreen
-        ></iframe>
-      </div>
-    </div>
-  </section>
-
-  <!-- DW LEDs Controls -->
-  <section id="dw-leds-container" class="bg-white rounded-xl shadow-sm overflow-hidden hidden">
-    <div class="px-6 py-5 space-y-6">
-      <h2 class="text-slate-800 text-xl sm:text-2xl font-semibold leading-tight tracking-[-0.01em]">
-        DW LEDs Control
-      </h2>
-
-      <!-- Connection Status -->
-      <div id="dw-leds-status" class="p-4 rounded-lg bg-gray-100 border border-slate-200">
-        <div class="flex items-center gap-2">
-          <span class="material-icons text-gray-500">info</span>
-          <span class="text-sm text-gray-700">Checking connection...</span>
-        </div>
-      </div>
-
-      <!-- Power and Brightness Grid -->
-      <div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
-        <!-- Power Control -->
-        <div class="flex flex-col gap-3">
-          <h3 class="text-slate-800 text-base font-semibold">Power</h3>
-          <button id="dw-leds-power-toggle" class="flex items-center justify-center gap-2 rounded-lg bg-green-600 px-4 py-3 text-sm font-semibold text-white shadow-md hover:bg-green-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-green-400 focus:ring-offset-2">
-            <span class="material-icons">power_settings_new</span>
-            <span id="dw-leds-power-text">Turn ON</span>
-          </button>
-        </div>
-
-        <!-- Brightness Control -->
-        <div class="flex flex-col gap-3">
-          <div class="flex items-center justify-between">
-            <h3 class="text-slate-800 text-base font-semibold">Brightness</h3>
-            <span id="dw-leds-brightness-value" class="text-sm font-medium text-slate-600">35%</span>
-          </div>
-          <input type="range" id="dw-leds-brightness" min="0" max="100" value="35" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
-        </div>
-      </div>
-
-      <!-- Effects and Palettes -->
-      <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
-        <div class="flex flex-col gap-3">
-          <h3 class="text-slate-800 text-base font-semibold">Effect</h3>
-          <select id="dw-leds-effect-select" class="form-select w-full rounded-lg border border-slate-300 bg-white px-4 py-3 text-slate-900 focus:outline-0 focus:ring-2 focus:ring-blue-500">
-            <option value="">Loading effects...</option>
-          </select>
-        </div>
-        <div class="flex flex-col gap-3">
-          <h3 class="text-slate-800 text-base font-semibold">Palette</h3>
-          <select id="dw-leds-palette-select" class="form-select w-full rounded-lg border border-slate-300 bg-white px-4 py-3 text-slate-900 focus:outline-0 focus:ring-2 focus:ring-blue-500">
-            <option value="">Loading palettes...</option>
-          </select>
-        </div>
-      </div>
-
-      <!-- Speed and Intensity -->
-      <div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
-        <!-- Speed Control -->
-        <div class="flex flex-col gap-3">
-          <div class="flex items-center justify-between">
-            <h3 class="text-slate-800 text-base font-semibold">Speed</h3>
-            <span id="dw-leds-speed-value" class="text-sm font-medium text-slate-600">128</span>
-          </div>
-          <input type="range" id="dw-leds-speed" min="0" max="255" value="128" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
-        </div>
-
-        <!-- Intensity Control -->
-        <div class="flex flex-col gap-3">
-          <div class="flex items-center justify-between">
-            <h3 class="text-slate-800 text-base font-semibold">Intensity</h3>
-            <span id="dw-leds-intensity-value" class="text-sm font-medium text-slate-600">128</span>
-          </div>
-          <input type="range" id="dw-leds-intensity" min="0" max="255" value="128" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
-        </div>
-      </div>
-
-      <!-- Effect Color Slots -->
-      <div class="flex items-center gap-4">
-        <h3 class="text-slate-800 text-base font-semibold">Effect Colors:</h3>
-        <div class="flex items-center gap-2">
-          <label class="text-xs font-medium text-slate-600">Slot 1</label>
-          <input type="text" id="dw-leds-color1" value="#ff0000" class="w-8 h-8 rounded-full border-2 border-slate-300 cursor-pointer effect-color-picker text-transparent" readonly>
-        </div>
-        <div class="flex items-center gap-2">
-          <label class="text-xs font-medium text-slate-600">Slot 2</label>
-          <input type="text" id="dw-leds-color2" value="#000000" class="w-8 h-8 rounded-full border-2 border-slate-300 cursor-pointer effect-color-picker text-transparent" readonly>
-        </div>
-        <div class="flex items-center gap-2">
-          <label class="text-xs font-medium text-slate-600">Slot 3</label>
-          <input type="text" id="dw-leds-color3" value="#0000ff" class="w-8 h-8 rounded-full border-2 border-slate-300 cursor-pointer effect-color-picker text-transparent" readonly>
-        </div>
-      </div>
-
-      <!-- Effect Settings -->
-      <div class="flex flex-col gap-6 pt-4 border-t border-slate-200">
-        <div>
-          <h3 class="text-slate-800 text-base font-semibold">Automation Settings</h3>
-          <p class="text-xs text-slate-500 mt-1">Configure LED effects to automatically activate when idle or playing patterns</p>
-        </div>
-
-        <!-- Playing Effect Configuration -->
-        <div class="bg-slate-50 rounded-lg p-4 border border-slate-200">
-          <div class="flex items-center justify-between mb-3">
-            <h4 class="text-slate-700 text-sm font-semibold flex items-center gap-2">
-              <span class="material-icons text-green-600 text-lg">play_circle</span>
-              Playing Effect
-            </h4>
-            <div class="flex gap-2">
-              <button id="dw-leds-save-current-playing" class="flex items-center gap-1.5 rounded-lg bg-green-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-green-700 transition-colors focus:outline-none focus:ring-2 focus:ring-green-400">
-                <span class="material-icons text-sm">save</span>
-                Save Current
-              </button>
-              <button id="dw-leds-clear-playing" class="flex items-center gap-1.5 rounded-lg bg-gray-500 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-gray-600 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-400">
-                <span class="material-icons text-sm">clear</span>
-                Clear
-              </button>
-            </div>
-          </div>
-
-          <div id="playing-current-settings" class="text-xs text-slate-600 p-2 bg-white rounded border border-slate-200">
-            <span class="font-medium">Current:</span> <span id="playing-settings-display">Not configured</span>
-          </div>
-        </div>
-
-        <!-- Idle Configuration (Effect + Timeout) -->
-        <div class="bg-slate-50 rounded-lg p-4 border border-slate-200">
-          <div class="flex items-center justify-between mb-4">
-            <h4 class="text-slate-700 text-sm font-semibold flex items-center gap-2">
-              <span class="material-icons text-blue-600 text-lg">bedtime</span>
-              Idle Configuration
-            </h4>
-          </div>
-
-          <!-- Idle Effect -->
-          <div class="mb-4 pb-4 border-b border-slate-200">
-            <div class="flex items-center justify-between mb-3">
-              <label class="text-xs font-medium text-slate-600">Idle Effect</label>
-              <div class="flex gap-2">
-                <button id="dw-leds-save-current-idle" class="flex items-center gap-1.5 rounded-lg bg-green-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-green-700 transition-colors focus:outline-none focus:ring-2 focus:ring-green-400">
-                  <span class="material-icons text-sm">save</span>
-                  Save Current
-                </button>
-                <button id="dw-leds-clear-idle" class="flex items-center gap-1.5 rounded-lg bg-gray-500 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-gray-600 transition-colors focus:outline-none focus:ring-2 focus:ring-gray-400">
-                  <span class="material-icons text-sm">clear</span>
-                  Clear
-                </button>
-              </div>
-            </div>
-            <div id="idle-current-settings" class="text-xs text-slate-600 p-2 bg-white rounded border border-slate-200">
-              <span class="font-medium">Current:</span> <span id="idle-settings-display">Not configured</span>
-            </div>
-          </div>
-
-          <!-- Idle Timeout -->
-          <div>
-            <div class="flex items-center justify-between mb-3">
-              <label class="text-xs font-medium text-slate-600 flex items-center gap-2">
-                <span class="material-icons text-blue-600 text-base">schedule</span>
-                Auto Turn Off
-              </label>
-              <label class="relative inline-flex items-center cursor-pointer" title="Enable automatic LED turn off after idle period">
-                <input type="checkbox" id="dw-leds-idle-timeout-enabled" class="sr-only peer">
-                <div class="w-11 h-6 bg-slate-300 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
-              </label>
-            </div>
-
-            <!-- Help text when disabled -->
-            <div id="idle-timeout-disabled-help" class="hidden mb-3 p-3 bg-blue-50 border border-blue-200 rounded-lg">
-              <div class="flex items-start gap-2">
-                <span class="material-icons text-blue-600 text-sm">info</span>
-                <p class="text-xs text-blue-700">
-                  <span class="font-semibold">Enable the toggle above</span> to automatically turn off LEDs after a period of inactivity.
-                </p>
-              </div>
-            </div>
-
-            <div id="idle-timeout-settings" class="space-y-3 transition-opacity duration-200">
-              <div>
-                <label class="block text-xs text-slate-500 mb-2">Turn off LEDs after</label>
-                <div class="flex items-center gap-3">
-                  <input type="number" id="dw-leds-idle-timeout-minutes" min="1" max="1440" value="30"
-                         class="flex-1 rounded-lg border-slate-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 text-sm">
-                  <span class="text-sm text-slate-600 whitespace-nowrap">minutes idle</span>
-                </div>
-              </div>
-
-              <div id="idle-timeout-remaining" class="text-xs text-slate-600 p-2 bg-white rounded border border-slate-200 hidden">
-                <span class="font-medium">Time remaining:</span> <span id="idle-timeout-remaining-display">--</span>
-              </div>
-
-              <button id="dw-leds-save-idle-timeout" class="w-full flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-700 transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400">
-                <span class="material-icons text-base">save</span>
-                Save Timeout Settings
-              </button>
-            </div>
-          </div>
-        </div>
-
-        <div class="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 class="text-xs text-blue-700">
-              <p class="font-medium text-blue-800">How to use:</p>
-              <ul class="mt-1 space-y-1 list-disc list-inside">
-                <li>Adjust LED settings above (effect, palette, speed, intensity, colors)</li>
-                <li>Click "Save Current" to capture current settings for automation</li>
-                <li>Click "Clear" to turn off automation for that state</li>
-                <li>Settings are applied automatically when table changes state</li>
-              </ul>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </section>
-
-</div>
-
-<script src="/static/js/coloris.min.js"></script>
-<script src="/static/js/led-control.js"></script>
-{% endblock %}

+ 0 - 443
templates/playlists.html

@@ -1,443 +0,0 @@
-{% extends "base.html" %}
-
-{% block title %}Playlists - {{ app_name or 'Dune Weaver' }}{% endblock %}
-
-{% block additional_styles %}
-/* Minimal custom styles - rely on Tailwind utilities */
-.pattern-preview {
-  border: 1px solid #e2e8f0;
-  overflow: hidden;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-
-.dark .pattern-preview img {
-  filter: invert(1);
-}
-
-/* Fix checkbox visibility in light mode */
-html:not(.dark) input[type="checkbox"]:checked {
-  background-color: #2563eb !important; /* Darker blue for better contrast */
-  border-color: #2563eb !important;
-}
-
-html:not(.dark) input[type="checkbox"] {
-  background-color: #ffffff !important;
-  border-color: #d1d5db !important;
-}
-
-/* Mobile responsive utilities */
-@media (max-width: 768px) {
-  .mobile-hidden {
-    display: none !important;
-  }
-  
-  .mobile-show {
-    display: block !important;
-  }
-  
-  .mobile-flex {
-    display: flex !important;
-  }
-  
-  .mobile-playlist-container {
-    padding-top: 0.5rem;
-    padding-bottom: 75px;
-    padding-left: 0;
-    padding-right: 0;
-  }
-  
-  .mobile-full-width {
-    width: 100% !important;
-    max-width: 100% !important;
-    border-radius: 0 !important;
-    border-left: none !important;
-    border-right: none !important;
-  }
-  
-  .mobile-patterns-grid {
-    grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)) !important;
-    gap: 0.75rem !important;
-    padding: 1rem !important;
-  }
-}
-
-.mobile-playlists-sidebar {
-  padding: 0 !important;
-  border-radius: 0 !important;
-  border-left: none !important;
-  border-right: none !important;
-  border-bottom: none !important;
-}
-
-/* Override base template's dark mode rules for playlists page in light mode */
-html:not(.dark) #playlistsSidebar,
-html:not(.dark) #playlistDetails,
-html:not(.dark) #playlistsNav,
-html:not(.dark) .bg-white {
-  background-color: #ffffff !important;
-}
-
-html:not(.dark) .bg-gray-50 {
-  background-color: #f9fafb !important;
-}
-
-html:not(.dark) .bg-gray-100 {
-  background-color: #f3f4f6 !important;
-}
-
-html:not(.dark) .text-gray-900 {
-  color: #111827 !important;
-}
-
-html:not(.dark) .text-gray-700 {
-  color: #374151 !important;
-}
-
-html:not(.dark) .text-gray-500 {
-  color: #6b7280 !important;
-}
-
-html:not(.dark) .border-gray-200 {
-  border-color: #e5e7eb !important;
-}
-
-html:not(.dark) .border-gray-300 {
-  border-color: #d1d5db !important;
-}
-
-/* Fix hover states in light mode */
-html:not(.dark) .hover\:bg-gray-100:hover,
-html:not(.dark) #playlistsNav a:hover {
-  background-color: #f3f4f6 !important;
-}
-
-html:not(.dark) .hover\:bg-gray-200:hover {
-  background-color: #e5e7eb !important;
-}
-
-html:not(.dark) .hover\:text-gray-900:hover {
-  color: #111827 !important;
-}
-
-html:not(.dark) .hover\:text-gray-700:hover {
-  color: #374151 !important;
-}
-
-/* Ensure active playlist item styling works in light mode */
-html:not(.dark) #playlistsNav a.active,
-html:not(.dark) .bg-gray-100 {
-  background-color: #f3f4f6 !important;
-  color: #111827 !important;
-}
-
-/* Fix pattern text colors in light mode */
-html:not(.dark) .text-gray-800 {
-  color: #1f2937 !important;
-}
-
-html:not(.dark) .group:hover .group-hover\:text-gray-900,
-html:not(.dark) .group-hover\:text-gray-900 {
-  color: #111827 !important;
-}
-
-/* Pattern cards text in light mode */
-html:not(.dark) #patternsGrid p,
-html:not(.dark) #patternsGrid .text-sm {
-  color: #1f2937 !important;
-}
-
-html:not(.dark) #patternsGrid .group:hover p,
-html:not(.dark) #patternsGrid .group:hover .text-sm {
-  color: #111827 !important;
-}
-
-/* Available patterns modal text in light mode */
-html:not(.dark) #availablePatternsGrid p,
-html:not(.dark) #availablePatternsGrid .text-xs {
-  color: #1f2937 !important;
-}
-{% endblock %}
-
-{% block content %}
-<div class="flex flex-1 justify-center p-4 sm:p-6 gap-6 bg-gray-50 dark:bg-gray-900 max-w-5xl mx-auto w-full mobile-playlist-container" style="min-height: calc(100vh - 72px - 64px);">
-  
-  <!-- Sidebar for Playlists -->
-  <aside id="playlistsSidebar" class="w-80 bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 flex flex-col mobile-full-width mobile-playlists-sidebar" style="height: calc(100vh - 72px - 64px - 48px);">
-    <div class="flex items-center justify-between gap-3 p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0 bg-white dark:bg-gray-800">
-      <h3 class="text-gray-900 dark:text-gray-100 text-xl font-semibold leading-tight">Playlists</h3>
-      <button id="addPlaylistBtn" class="flex items-center justify-center size-8 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors duration-150">
-        <span class="material-icons text-xl">add</span>
-      </button>
-    </div>
-    <nav id="playlistsNav" class="flex-1 overflow-y-auto p-3 space-y-1 bg-white dark:bg-gray-800">
-      <!-- Playlists will be populated here -->
-      <div class="flex items-center justify-center py-8 text-gray-500 dark:text-gray-400">
-        <span class="text-sm">Loading playlists...</span>
-      </div>
-    </nav>
-  </aside>
-
-  <!-- Main Content -->
-  <main id="playlistDetails" class="flex-1 bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col mobile-full-width" style="height: calc(100vh - 72px - 64px - 48px);">
-    <header class="flex items-center justify-between gap-4 p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0 bg-white dark:bg-gray-800">
-      <!-- Mobile back button in header -->
-      <div class="flex items-center gap-3 flex-1 min-w-0">
-        <button id="mobileBackBtn" class="hidden mobile-flex md:hidden items-center justify-center size-8 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors duration-150 flex-shrink-0">
-          <span class="material-icons text-lg">arrow_back</span>
-        </button>
-        <div id="currentPlaylistTitle" class="flex items-center gap-3 min-w-0 flex-1">
-          <h1 class="text-gray-900 dark:text-gray-100 text-2xl font-semibold leading-tight truncate">Select a Playlist</h1>
-        </div>
-      </div>
-      <button id="addPatternsBtn" class="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg transition-colors duration-150 disabled:opacity-50 disabled:cursor-not-allowed flex-shrink-0" disabled>
-        <span class="material-icons text-lg">add_photo_alternate</span>
-        <span class="hidden sm:inline">Add Patterns</span>
-        <span class="sm:hidden">Add</span>
-      </button>
-    </header>
-    
-    <div class="flex-1 flex flex-col overflow-hidden bg-white dark:bg-gray-800">
-      <!-- Patterns Grid - Scrollable -->
-      <div class="flex-1 overflow-y-auto border-b border-gray-200 dark:border-gray-700">
-        <div id="patternsGrid" class="grid grid-cols-[repeat(auto-fill,minmax(128px,1fr))] gap-4 p-4 mobile-patterns-grid">
-          <div class="flex items-center justify-center col-span-full py-12 text-gray-500 dark:text-gray-400">
-            <span class="text-sm text-center">Select a playlist to view its patterns</span>
-          </div>
-        </div>
-      </div>
-
-      <!-- Playback Settings - Fixed at bottom -->
-      <div id="playbackSettings" class="bg-white dark:bg-gray-800 flex-shrink-0 hidden">
-        <div class="px-4 py-4 border-b border-gray-200 dark:border-gray-700">
-          <h2 class="text-gray-900 dark:text-gray-100 text-base font-semibold mb-3">Playback Settings</h2>
-          
-          <div class="space-y-4">
-            <!-- Run Mode & Shuffle Section -->
-            <div class="pb-3 border-b border-gray-100 dark:border-gray-600">
-              <div class="flex flex-wrap items-center gap-3 mb-3">
-                <div class="flex gap-2">
-                  <label class="text-xs font-medium flex items-center justify-center rounded-md border-2 border-gray-300 dark:border-gray-600 px-3 h-8 text-gray-700 dark:text-gray-300 has-[:checked]:border-blue-500 has-[:checked]:bg-blue-50 dark:has-[:checked]:bg-blue-900/20 has-[:checked]:text-blue-700 dark:has-[:checked]:text-blue-300 has-[:checked]:font-semibold relative cursor-pointer transition-all duration-200 hover:border-gray-400 dark:hover:border-gray-500">
-                    <span class="material-icons text-sm mr-1">play_circle</span>
-                    Once
-                    <input class="invisible absolute" name="run_playlist" type="radio" value="single" checked/>
-                  </label>
-                  <label class="text-xs font-medium flex items-center justify-center rounded-md border-2 border-gray-300 dark:border-gray-600 px-3 h-8 text-gray-700 dark:text-gray-300 has-[:checked]:border-blue-500 has-[:checked]:bg-blue-50 dark:has-[:checked]:bg-blue-900/20 has-[:checked]:text-blue-700 dark:has-[:checked]:text-blue-300 has-[:checked]:font-semibold relative cursor-pointer transition-all duration-200 hover:border-gray-400 dark:hover:border-gray-500">
-                    <span class="material-icons text-sm mr-1">repeat</span>
-                    Loop
-                    <input class="invisible absolute" name="run_playlist" type="radio" value="indefinite"/>
-                  </label>
-                </div>
-                
-                <div class="flex items-center gap-2">
-                  <input id="shuffleCheckbox" type="checkbox" class="h-4 w-4 text-blue-600 bg-gray-100 dark:bg-gray-700 border-gray-300 dark:border-gray-600 rounded focus:ring-blue-500 dark:focus:ring-blue-600">
-                  <label for="shuffleCheckbox" class="text-xs font-medium text-gray-700 dark:text-gray-300 select-none cursor-pointer flex items-center gap-1">
-                    <span class="material-icons text-sm">shuffle</span>
-                    Shuffle
-                  </label>
-                </div>
-              </div>
-            </div>
-
-            <!-- Timing & Clear Pattern Section -->
-            <div class="pb-3 border-b border-gray-100 dark:border-gray-600">
-              <div class="grid grid-cols-2 gap-3">
-                <div class="flex items-center gap-2">
-                  <span class="material-icons text-sm text-gray-500 dark:text-gray-400">schedule</span>
-                  <span class="text-xs font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap">Pause:</span>
-                  <div class="relative flex-1">
-                    <input id="pauseTimeInput" class="w-full rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 h-8 px-2 pr-8 text-xs" type="number" value="5" min="0" step="1"/>
-                    <span class="absolute right-2 top-1/2 transform -translate-y-1/2 text-xs text-gray-500 dark:text-gray-400">s</span>
-                  </div>
-                </div>
-                
-                <div class="flex items-center gap-2">
-                  <span class="material-icons text-sm text-gray-500 dark:text-gray-400">clear_all</span>
-                  <span class="text-xs font-medium text-gray-700 dark:text-gray-300 whitespace-nowrap">Clear:</span>
-                  <select id="clearPatternSelect" class="flex-1 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 h-8 px-2 text-xs">
-                    <option value="adaptive">Adaptive</option>
-                    <option value="clear_from_in">Center</option>
-                    <option value="clear_from_out">Perimeter</option>
-                    <option value="clear_sideway">Sideway</option>
-                    <option value="none">None</option>
-                  </select>
-                </div>
-              </div>
-            </div>
-
-            <!-- Run Button Section -->
-            <div class="pt-1">
-              <button id="runPlaylistBtn" class="flex items-center justify-center gap-2 w-full h-9 px-4 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:hover:bg-gray-400 text-white text-sm font-medium rounded-md transition-colors duration-200 disabled:opacity-60 disabled:cursor-not-allowed" disabled>
-                <span class="material-icons text-base">play_arrow</span>
-                <span>Run Playlist</span>
-              </button>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </main>
-</div>
-
-<!-- Add Playlist Modal -->
-<div id="addPlaylistModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
-  <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4">
-    <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Create New Playlist</h3>
-    <div class="space-y-4">
-      <div>
-        <label for="newPlaylistName" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Playlist Name</label>
-        <input id="newPlaylistName" type="text" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 px-3 py-2" placeholder="Enter playlist name" autofocus>
-      </div>
-      <div class="flex gap-3 justify-end">
-        <button id="cancelPlaylistBtn" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors duration-150">Cancel</button>
-        <button id="createPlaylistBtn" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors duration-150">Create</button>
-      </div>
-    </div>
-  </div>
-</div>
-
-<!-- Rename Playlist Modal -->
-<div id="renamePlaylistModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
-  <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-md mx-4">
-    <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Rename Playlist</h3>
-    <div class="space-y-4">
-      <div>
-        <label for="renamePlaylistInput" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">New Name</label>
-        <input id="renamePlaylistInput" type="text" class="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 px-3 py-2" placeholder="Enter new playlist name">
-      </div>
-      <div class="flex gap-3 justify-end">
-        <button id="cancelRenameBtn" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors duration-150">Cancel</button>
-        <button id="confirmRenameBtn" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors duration-150">Rename</button>
-      </div>
-    </div>
-  </div>
-</div>
-
-<!-- Add Patterns Modal -->
-<div id="addPatternsModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
-  <div class="bg-white dark:bg-gray-800 rounded-lg p-6 w-full max-w-4xl mx-4 max-h-[80vh] overflow-hidden flex flex-col">
-    <div class="flex items-center justify-between mb-4 flex-shrink-0">
-      <h3 id="modalTitle" class="text-lg font-semibold text-gray-900 dark:text-gray-100">Add Patterns to Playlist</h3>
-    </div>
-    
-    <!-- Search Bar and Controls -->
-    <div class="mb-4 flex-shrink-0 space-y-3">
-      <div class="relative">
-        <span class="material-icons absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-500 text-lg">search</span>
-        <input 
-          id="patternSearchInput" 
-          type="text" 
-          placeholder="Search patterns..." 
-          class="w-full pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-sm"
-        />
-        <button id="clearSearchBtn" class="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 hidden">
-          <span class="material-icons text-lg">clear</span>
-        </button>
-      </div>
-      
-      <!-- Sort and Filter Controls -->
-      <div class="flex flex-wrap gap-3 items-center">
-        <div class="flex items-center gap-2">
-          <span class="text-xs font-medium text-gray-700 dark:text-gray-300">Sort by:</span>
-          <select id="sortFieldSelect" class="text-xs rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1">
-            <option value="name">Name</option>
-            <option value="date">Date Modified</option>
-            <option value="coordinates">Coordinates</option>
-            <option value="favorite">Favorite</option>
-          </select>
-          <button id="sortDirectionBtn" class="p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-500 dark:text-gray-400" title="Toggle sort direction">
-            <span class="material-icons text-sm" id="sortDirectionIcon">arrow_upward</span>
-          </button>
-        </div>
-        
-        <div class="flex items-center gap-2">
-          <span class="text-xs font-medium text-gray-700 dark:text-gray-300">Folder:</span>
-          <select id="categoryFilterSelect" class="text-xs rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 px-2 py-1">
-            <option value="all">All</option>
-          </select>
-        </div>
-      </div>
-      
-      <!-- Smart Toggle Select All button -->
-      <div class="flex items-center justify-between">
-        <button 
-          id="toggleSelectAllBtn" 
-          class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors duration-150"
-        >
-          <span class="material-icons text-base" id="toggleSelectAllIcon">check_box_outline_blank</span>
-          <span id="toggleSelectAllText">Select All</span>
-        </button>
-        <span id="selectionCount" class="text-xs text-gray-500 dark:text-gray-400">
-          0 selected
-        </span>
-      </div>
-    </div>
-    
-    <div class="flex-1 overflow-y-auto">
-      <div id="availablePatternsGrid" class="grid grid-cols-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3 sm:gap-4 p-2">
-        <!-- Available patterns will be populated here -->
-      </div>
-      
-      <!-- Loading indicator -->
-      <div id="patternsLoadingIndicator" class="flex items-center justify-center py-12 text-gray-500 dark:text-gray-400 hidden">
-        <div class="flex items-center gap-3">
-          <div class="bg-gray-200 dark:bg-gray-700 rounded-full h-6 w-6 flex items-center justify-center">
-            <div class="bg-gray-500 dark:bg-gray-400 rounded-full h-3 w-3"></div>
-          </div>
-          <span class="text-sm">Loading patterns...</span>
-        </div>
-      </div>
-      
-      <!-- No results message -->
-      <div id="noResultsMessage" class="flex items-center justify-center py-12 text-gray-500 dark:text-gray-400 hidden">
-        <span class="text-sm">No patterns found matching your search</span>
-      </div>
-    </div>
-    
-    <div class="flex gap-3 justify-end mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0">
-      <button id="cancelAddPatternsBtn" class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-lg transition-colors duration-150">Cancel</button>
-      <button id="confirmAddPatternsBtn" class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors duration-150">Save</button>
-    </div>
-  </div>
-</div>
-{% endblock %}
-
-{% block scripts %}
-<script>
-// Force light mode styling if not in dark mode
-document.addEventListener('DOMContentLoaded', function() {
-  const isDark = document.documentElement.classList.contains('dark');
-  
-  if (!isDark) {
-    // Force light backgrounds
-    const sidebar = document.getElementById('playlistsSidebar');
-    const mainContent = document.getElementById('playlistDetails');
-    const nav = document.getElementById('playlistsNav');
-    
-    if (sidebar) {
-      sidebar.style.backgroundColor = '#ffffff';
-      sidebar.style.borderColor = '#e5e7eb';
-    }
-    
-    if (mainContent) {
-      mainContent.style.backgroundColor = '#ffffff';
-      mainContent.style.borderColor = '#e5e7eb';
-    }
-    
-    if (nav) {
-      nav.style.backgroundColor = '#ffffff';
-    }
-    
-    // Force light text colors
-    document.querySelectorAll('h1, h2, h3').forEach(el => {
-      if (!el.closest('.dark')) {
-        el.style.color = '#111827';
-      }
-    });
-  }
-});
-</script>
-<script src="/static/js/playlists.js"></script>
-{% endblock %} 

+ 0 - 1552
templates/settings.html

@@ -1,1552 +0,0 @@
-{% extends "base.html" %} {% block title %}Settings - {{ app_name or 'Dune Weaver' }}{%
-endblock %}
-
-{% block additional_styles %}
-/* Dark mode styles for settings page */
-.dark .bg-white {
-  background-color: #262626;
-}
-.dark .text-slate-900 {
-  color: #ffffff;
-}
-.dark .text-slate-800 {
-  color: #f8fafc;
-}
-.dark .text-slate-700 {
-  color: #f1f5f9;
-}
-.dark .text-slate-600 {
-  color: #e2e8f0;
-}
-.dark .text-slate-500 {
-  color: #e2e8f0;
-}
-
-/* Label overrides for better visibility */
-.dark label {
-  color: #f1f5f9;
-}
-.dark .border-slate-200 {
-  border-color: #404040;
-}
-.dark .border-slate-300 {
-  border-color: #404040;
-}
-.dark .divide-slate-100 {
-  border-color: #333333;
-}
-.dark .bg-slate-50 {
-  background-color: #262626;
-}
-.dark .hover\:bg-slate-50:hover {
-  background-color: #404040;
-}
-.dark .bg-slate-100 {
-  background-color: #404040;
-}
-.dark .form-input,
-.dark input[type="number"],
-.dark input[type="text"],
-.dark input[type="time"] {
-  background-color: #1f1f1f;
-  border-color: #404040;
-  color: #e5e5e5;
-}
-.dark .form-input::placeholder {
-  color: #9ca3af;
-}
-.dark .form-input:focus {
-  border-color: #0c7ff2;
-  ring-color: #0c7ff2;
-}
-.dark input[type="time"]::-webkit-calendar-picker-indicator {
-  filter: invert(1);
-}
-.dark .form-select {
-  background-color: #1f1f1f;
-  border-color: #404040;
-  color: #e5e5e5;
-}
-.dark .form-select:focus {
-  border-color: #0c7ff2;
-  ring-color: #0c7ff2;
-}
-.dark .focus\:ring-sky-500:focus {
-  ring-color: #0c7ff2;
-}
-.dark .focus\:border-sky-500:focus {
-  border-color: #0c7ff2;
-}
-.dark .hover\:text-gray-700:hover {
-  color: #e5e5e5;
-}
-.dark .text-gray-400 {
-  color: #9ca3af;
-}
-
-/* Autocomplete suggestions dark mode */
-.dark #clearFromInSuggestions,
-.dark #clearFromOutSuggestions {
-  background-color: #262626;
-  border-color: #404040;
-}
-.dark .suggestion-item {
-  color: #e5e5e5;
-}
-.dark .suggestion-item:hover {
-  background-color: #404040;
-}
-.dark .suggestion-item.selected {
-  background-color: #0c7ff2;
-  color: white;
-}
-
-/* Light mode autocomplete styles */
-.suggestion-item {
-  padding: 8px 12px;
-  cursor: pointer;
-  color: #1f2937;
-  transition: background-color 0.15s;
-}
-.suggestion-item:hover {
-  background-color: #f3f4f6;
-}
-.suggestion-item.selected {
-  background-color: #0c7ff2;
-  color: white;
-}
-.suggestion-item mark {
-  background-color: #fef3c7;
-  font-weight: 600;
-}
-.dark .suggestion-item mark {
-  background-color: #92400e;
-  color: #fef3c7;
-}
-
-/* Toggle switch styles */
-.switch {
-  position: relative;
-  display: inline-block;
-  width: 60px;
-  height: 34px;
-}
-
-.switch input {
-  opacity: 0;
-  width: 0;
-  height: 0;
-}
-
-.slider {
-  position: absolute;
-  cursor: pointer;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background-color: #ccc;
-  transition: .4s;
-}
-
-.slider:before {
-  position: absolute;
-  content: "";
-  height: 26px;
-  width: 26px;
-  left: 4px;
-  bottom: 4px;
-  background-color: white;
-  transition: .4s;
-}
-
-input:checked + .slider {
-  background-color: #0c7ff2;
-}
-
-input:focus + .slider {
-  box-shadow: 0 0 1px #0c7ff2;
-}
-
-input:checked + .slider:before {
-  transform: translateX(26px);
-}
-
-.slider.round {
-  border-radius: 34px;
-}
-
-.slider.round:before {
-  border-radius: 50%;
-}
-
-/* Dark mode for switches */
-.dark .slider {
-  background-color: #404040;
-}
-
-.dark input:checked + .slider {
-  background-color: #0c7ff2;
-}
-
-/* Spin animation for loading states */
-@keyframes spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}
-
-.animate-spin {
-  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-item {
-  background-color: #f8fafc;
-  border: 1px solid #e2e8f0;
-  border-radius: 8px;
-  padding: 16px;
-  transition: all 0.15s;
-}
-
-.dark .time-slot-item {
-  background-color: #1e293b;
-  border-color: #475569;
-}
-
-.time-slot-item:hover {
-  border-color: #cbd5e1;
-}
-
-.dark .time-slot-item:hover {
-  border-color: #64748b;
-}
-
-/* Info box dark mode - grey theme */
-.dark .bg-blue-50 {
-  background-color: #1f1f1f;
-}
-
-.dark .border-blue-200 {
-  border-color: #404040;
-}
-
-.dark .text-blue-600 {
-  color: #e2e8f0;
-}
-
-.dark .text-blue-800 {
-  color: #f1f5f9;
-}
-
-.dark .text-blue-700 {
-  color: #e2e8f0;
-}
-
-/* Amber box dark mode - grey theme */
-.dark .bg-amber-50 {
-  background-color: #1f1f1f;
-}
-
-.dark .border-amber-200 {
-  border-color: #404040;
-}
-
-.dark .text-amber-600 {
-  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 %}
-
-{% block content %}
-<div class="layout-content-container flex flex-col w-full max-w-4xl gap-8 pt-2 pb-[75px]">
-  <div
-    class="flex flex-wrap justify-between items-center p-4 bg-white rounded-xl shadow-sm mt-2 sm:mt-8"
-  >
-    <h1
-      class="text-slate-900 tracking-tight text-2xl sm:text-3xl font-bold leading-tight"
-    >
-      Settings
-    </h1>
-  </div>
-  <section class="bg-white rounded-xl shadow-sm overflow-hidden">
-    <h2
-      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)"
-    >
-      <span>Device Connection</span>
-      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
-    </h2>
-    <div class="section-content">
-      <div
-        class="flex items-center gap-4 px-6 py-5 hover:bg-slate-50 transition-colors"
-      >
-        <div
-          class="text-slate-600 flex items-center justify-center rounded-lg bg-slate-100 shrink-0 size-12"
-        >
-          <span class="material-icons text-3xl">usb_off</span>
-        </div>
-        <div class="flex-1">
-          <p class="text-slate-800 text-base font-medium leading-normal">
-            Status
-          </p>
-          <p
-            id="serialStatus"
-            class="text-red-500 text-sm font-medium leading-normal"
-          >
-            Disconnected
-          </p>
-        </div>
-        <button
-          id="disconnectButton"
-          class="text-xs font-medium bg-red-100 hover:bg-red-200 text-red-700 dark:bg-red-900 dark:hover:bg-red-800 dark:text-red-200 px-3 py-1.5 rounded-md transition-colors"
-          hidden
-        >
-          Disconnect
-        </button>
-      </div>
-      <div id="portSelectionDiv" class="px-6 py-5 space-y-4">
-        <label class="flex flex-col gap-1.5">
-          <span class="text-slate-700 text-sm font-medium leading-normal"
-            >Available Serial Ports</span
-          >
-          <div class="flex gap-3 items-center">
-            <select
-              id="portSelect"
-              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="">Select a port...</option>
-            </select>
-            <button
-              id="connectButton"
-              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">cable</span>
-              <span class="truncate">Connect</span>
-            </button>
-          </div>
-          <p class="text-xs text-slate-500 mt-2">
-            Select a port and click 'Connect' to establish a connection.
-          </p>
-        </label>
-      </div>
-      <!-- Preferred Port Configuration -->
-      <div class="px-6 py-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="material-icons text-slate-600 text-base">star</span>
-            Preferred Port for Auto-Connect
-          </span>
-          <div class="flex gap-3 items-center">
-            <select
-              id="preferredPortSelect"
-              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="">No preference (auto-detect)</option>
-            </select>
-            <button
-              id="savePreferredPort"
-              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 mt-2">
-            When multiple ports are available, this port will be used automatically on startup. If set to "No preference", the system will try the last connected port or the first available port.
-          </p>
-          <p id="currentPreferredPort" class="text-xs text-sky-600 mt-1 hidden">
-            <span class="material-icons text-xs align-middle">check_circle</span>
-            <span id="preferredPortDisplay"></span>
-          </p>
-        </label>
-      </div>
-    </div>
-  </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 -->
-  <section id="homingSection" 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>Homing Configuration</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">
-      <!-- Homing Mode Selection -->
-      <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">home</span>
-          Homing Mode
-        </label>
-
-        <div class="space-y-3">
-          <!-- Crash Homing Option -->
-          <label class="flex items-start gap-3 p-3 border border-slate-300 rounded-lg cursor-pointer hover:bg-slate-50 transition-colors">
-            <input
-              type="radio"
-              name="homingMode"
-              value="0"
-              id="homingModeCrash"
-              class="mt-0.5 w-4 h-4 text-sky-600 focus:ring-sky-500"
-            />
-            <div class="flex-1">
-              <div class="text-sm font-medium text-slate-700">Crash Homing</div>
-              <div class="text-xs text-slate-500 mt-1">
-                Y axis moves until physical stop, then theta and rho set to 0 (no x0 y0 command)
-              </div>
-            </div>
-          </label>
-
-          <!-- Sensor Homing Option -->
-          <label class="flex items-start gap-3 p-3 border border-slate-300 rounded-lg cursor-pointer hover:bg-slate-50 transition-colors">
-            <input
-              type="radio"
-              name="homingMode"
-              value="1"
-              id="homingModeSensor"
-              class="mt-0.5 w-4 h-4 text-sky-600 focus:ring-sky-500"
-            />
-            <div class="flex-1">
-              <div class="text-sm font-medium text-slate-700">Sensor Homing</div>
-              <div class="text-xs text-slate-500 mt-1">
-                Homes both X and Y axes using sensors
-              </div>
-            </div>
-          </label>
-        </div>
-      </div>
-
-      <!-- Compass Reference Point (Sensor mode only) -->
-      <div id="compassOffsetContainer" class="space-y-2">
-        <label for="angularOffsetInput" class="text-sm font-medium text-slate-700 flex items-center gap-2">
-          <span class="material-icons text-slate-600 text-base">explore</span>
-          Sensor offset (degrees) <span class="text-xs text-slate-400">(Sensor mode only)</span>
-        </label>
-        <input
-          type="number"
-          id="angularOffsetInput"
-          min="0"
-          max="360"
-          step="0.1"
-          value="0"
-          class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-sky-500 focus:border-sky-500 text-sm"
-          placeholder="0.0"
-        />
-        <p class="text-xs text-slate-500">
-          Set the angle (in degrees) where your radial arm to be offset by. You want to choose a value here so that the radial arm will point East.
-        </p>
-      </div>
-
-      <!-- Homing Info Box -->
-      <div id="homingInfoBox" 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 id="homingInfoContent">
-            <p class="font-medium text-blue-800">Crash Homing Mode:</p>
-            <ul class="mt-1 space-y-1 text-blue-700">
-              <li>• Y axis moves -22mm (or -30mm for mini) until physical stop</li>
-              <li>• Theta set to 0, rho set to 0</li>
-              <li>• No x0 y0 command sent</li>
-              <li>• No hardware sensors required</li>
-            </ul>
-          </div>
-        </div>
-      </div>
-
-      <!-- Auto-Home During Playlists -->
-      <div class="bg-slate-50 rounded-lg p-4 space-y-4">
-        <div class="flex items-center justify-between">
-          <div class="flex-1">
-            <h3 class="text-slate-800 text-base font-semibold flex items-center gap-2">
-              <span class="material-icons text-slate-600 text-base">autorenew</span>
-              Auto-Home During Playlists
-            </h3>
-            <p class="text-xs text-slate-500 mt-1">
-              Automatically perform homing after a certain number of patterns during playlist playback to maintain accuracy.
-            </p>
-          </div>
-          <label class="switch">
-            <input type="checkbox" id="autoHomeEnabledToggle">
-            <span class="slider round"></span>
-          </label>
-        </div>
-
-        <div id="autoHomeSettings" style="display: none;">
-          <label class="flex flex-col gap-1.5">
-            <span class="text-slate-700 text-sm font-medium leading-normal">Home after every X patterns</span>
-            <input
-              type="number"
-              id="autoHomeAfterPatternsInput"
-              min="1"
-              max="100"
-              step="1"
-              value="5"
-              class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-sky-500 focus:border-sky-500 text-sm"
-              placeholder="5"
-            />
-            <p class="text-xs text-slate-500">
-              Homing will occur right after the clear pattern completes, before the next actual pattern begins.
-            </p>
-          </label>
-        </div>
-      </div>
-
-      <div class="flex justify-end">
-        <button
-          id="saveHomingConfig"
-          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 Configuration</span>
-        </button>
-      </div>
-    </div>
-  </section>
-  <section 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>Application 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">
-      <label class="flex flex-col gap-1.5">
-        <span class="text-slate-700 text-sm font-medium leading-normal"
-          >Application Name</span
-        >
-        <div class="flex gap-3 items-center">
-          <div class="relative flex-1">
-            <input
-              id="appNameInput"
-              class="form-input flex w-full min-w-0 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 pr-10 text-base font-normal leading-normal transition-colors"
-              placeholder="e.g., Dune Weaver"
-              value="Dune Weaver"
-            />
-            <button
-              type="button"
-              onclick="document.getElementById('appNameInput').value='Dune Weaver';"
-              class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700"
-              aria-label="Reset to default"
-              title="Reset to default"
-            >
-              <span class="material-icons">restart_alt</span>
-            </button>
-          </div>
-          <button
-            id="saveAppName"
-            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 flex-shrink-0"
-          >
-            <span class="material-icons text-lg">save</span>
-            <span class="truncate">Save Name</span>
-          </button>
-        </div>
-        <p class="text-xs text-slate-500 mt-2">
-          This name will appear in the browser tab and at the top of every page.
-        </p>
-      </label>
-
-      <!-- Custom Logo Section -->
-      <div class="border-t border-slate-200 pt-6">
-        <label class="flex flex-col gap-1.5">
-          <span class="text-slate-700 text-sm font-medium leading-normal">Custom Logo & Favicon</span>
-          <p class="text-xs text-slate-500 mb-2">
-            Upload a custom logo to replace the default. The favicon (browser tab icon) will be automatically generated from your logo. Recommended size: 180x180 pixels. Supported formats: PNG, JPG, GIF, WebP, SVG.
-          </p>
-          <div class="flex gap-4 items-start">
-            <!-- Logo Preview -->
-            <div class="flex-shrink-0">
-              <div id="logoPreviewContainer" class="w-16 h-16 rounded-full shadow border border-slate-200 overflow-hidden bg-slate-100 flex items-center justify-center">
-                <img id="logoPreview" src="{% if custom_logo %}/static/custom/{{ custom_logo }}{% else %}/static/apple-touch-icon.png{% endif %}" alt="Logo Preview" class="w-full h-full object-cover"/>
-              </div>
-            </div>
-            <!-- Upload Controls -->
-            <div class="flex-1 space-y-2">
-              <div class="flex gap-2 flex-wrap">
-                <label class="flex items-center justify-center gap-2 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">upload</span>
-                  <span>Upload Logo</span>
-                  <input type="file" id="logoFileInput" accept=".png,.jpg,.jpeg,.gif,.webp,.svg" class="hidden" />
-                </label>
-                <button
-                  type="button"
-                  id="resetLogoBtn"
-                  class="flex items-center justify-center gap-2 cursor-pointer rounded-lg h-10 px-4 border border-slate-300 hover:bg-slate-50 text-slate-700 text-sm font-medium leading-normal transition-colors {% if not custom_logo %}hidden{% endif %}"
-                >
-                  <span class="material-icons text-lg">restart_alt</span>
-                  <span>Reset to Default</span>
-                </button>
-              </div>
-              <p id="logoUploadStatus" class="text-xs text-slate-500"></p>
-            </div>
-          </div>
-        </label>
-      </div>
-    </div>
-  </section>
-  <section 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>Pattern Clearing</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">
-      <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.
-      </p>
-
-      <!-- Clearing Speed Section -->
-      <div class="bg-slate-50 rounded-lg p-4 space-y-4">
-        <h3 class="text-slate-800 text-base font-semibold">Clearing Speed</h3>
-        <p class="text-sm text-slate-600">
-          Set a custom speed for clearing patterns. Leave empty to use the default pattern speed. This allows clearing patterns to run at a different speed than regular patterns.
-        </p>
-        <div class="flex flex-col gap-1.5">
-          <label for="clearPatternSpeedInput" class="text-slate-700 text-sm font-medium leading-normal">
-            Speed (steps per minute)
-          </label>
-          <div class="flex gap-3 items-center">
-            <input
-              id="clearPatternSpeedInput"
-              type="number"
-              min="50"
-              max="2000"
-              step="50"
-              class="form-input 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-normal leading-normal transition-colors"
-              placeholder="Default (use pattern speed)"
-              value=""
-            />
-            <button
-              id="saveClearSpeed"
-              class="flex items-center justify-center gap-2 min-w-[120px] 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 Speed</span>
-            </button>
-          </div>
-          <div id="effectiveClearSpeed" class="text-xs text-slate-500 mt-1"></div>
-        </div>
-      </div>
-
-      <!-- Custom Patterns Section -->
-      <div class="bg-slate-50 rounded-lg p-4 space-y-4">
-        <h3 class="text-slate-800 text-base font-semibold">Custom Clear Patterns</h3>
-        <p class="text-sm text-slate-600">
-          Choose specific patterns to use when clearing. Leave empty to use the default clearing behavior.
-        </p>
-        
-        <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
-        <div class="flex flex-col gap-1.5">
-          <label for="customClearFromInInput" class="text-slate-700 text-sm font-medium leading-normal">Clear From Center Pattern</label>
-          <div class="relative">
-            <input
-              id="customClearFromInInput"
-              type="text"
-              class="form-input w-full 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 pr-10 text-base font-normal leading-normal transition-colors"
-              placeholder="Type to search patterns or leave empty for default"
-              autocomplete="off"
-            />
-            <button
-              type="button"
-              id="clearFromInClear"
-              class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hidden"
-              aria-label="Clear selection"
-              title="Clear selection"
-            >
-              <span class="material-icons text-xl">close</span>
-            </button>
-            <div id="clearFromInSuggestions" class="absolute z-10 w-full mt-1 bg-white border border-slate-300 rounded-lg shadow-lg max-h-60 overflow-y-auto hidden"></div>
-          </div>
-          <p class="text-xs text-slate-500 mt-1">
-            Pattern to use when clearing from the center outward.
-          </p>
-        </div>
-
-        <div class="flex flex-col gap-1.5">
-          <label for="customClearFromOutInput" class="text-slate-700 text-sm font-medium leading-normal">Clear From Perimeter Pattern</label>
-          <div class="relative">
-            <input
-              id="customClearFromOutInput"
-              type="text"
-              class="form-input w-full 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 pr-10 text-base font-normal leading-normal transition-colors"
-              placeholder="Type to search patterns or leave empty for default"
-              autocomplete="off"
-            />
-            <button
-              type="button"
-              id="clearFromOutClear"
-              class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hidden"
-              aria-label="Clear selection"
-              title="Clear selection"
-            >
-              <span class="material-icons text-xl">close</span>
-            </button>
-            <div id="clearFromOutSuggestions" class="absolute z-10 w-full mt-1 bg-white border border-slate-300 rounded-lg shadow-lg max-h-60 overflow-y-auto hidden"></div>
-          </div>
-          <p class="text-xs text-slate-500 mt-1">
-            Pattern to use when clearing from the perimeter inward.
-          </p>
-        </div>
-      </div>
-
-        <div class="flex justify-end">
-          <button
-            id="saveClearPatterns"
-            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 Patterns</span>
-          </button>
-        </div>
-      </div>
-    </div>
-  </section>
-  <section 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>LED Controller Configuration</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">
-      <!-- LED Provider Selection -->
-      <div class="flex flex-col gap-2">
-        <span class="text-slate-700 text-sm font-medium leading-normal">LED Provider</span>
-        <div class="flex gap-3">
-          <label class="flex items-center gap-2 cursor-pointer">
-            <input type="radio" name="ledProvider" value="none" id="ledProviderNone" class="w-4 h-4 text-sky-600 border-slate-300 focus:ring-sky-500">
-            <span class="text-sm text-slate-700">None</span>
-          </label>
-          <label class="flex items-center gap-2 cursor-pointer">
-            <input type="radio" name="ledProvider" value="wled" id="ledProviderWled" class="w-4 h-4 text-sky-600 border-slate-300 focus:ring-sky-500">
-            <span class="text-sm text-slate-700">WLED</span>
-          </label>
-          <label class="flex items-center gap-2 cursor-pointer">
-            <input type="radio" name="ledProvider" value="dw_leds" id="ledProviderDwLeds" class="w-4 h-4 text-sky-600 border-slate-300 focus:ring-sky-500">
-            <span class="text-sm text-slate-700">DW LEDs (Local GPIO)</span>
-          </label>
-        </div>
-        <p class="text-xs text-slate-500">
-          Select your LED control system (settings are mutually exclusive)
-        </p>
-      </div>
-
-      <!-- WLED Configuration (shown when WLED is selected) -->
-      <div id="wledConfig" class="flex flex-col gap-4 hidden">
-        <label class="flex flex-col gap-1.5">
-          <span class="text-slate-700 text-sm font-medium leading-normal">WLED IP Address</span>
-          <div class="relative flex-1">
-            <input
-              id="wledIpInput"
-              class="form-input flex w-full min-w-0 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 pr-10 text-base font-normal leading-normal transition-colors"
-              placeholder="e.g., 192.168.1.100"
-              value=""
-            />
-            <button
-              type="button"
-              onclick="document.getElementById('wledIpInput').value='';"
-              class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700"
-              aria-label="Clear WLED IP"
-            >
-              <span class="material-icons">close</span>
-            </button>
-          </div>
-          <p class="text-xs text-slate-500">
-            Enter the IP address of your WLED controller
-          </p>
-        </label>
-      </div>
-
-      <!-- DW LEDs Configuration (shown when DW LEDs is selected) -->
-      <div id="dwLedsConfig" class="flex flex-col gap-4 hidden">
-        <div class="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 class="text-xs text-blue-700">
-              <p class="font-medium text-blue-800">Supported LED Strips</p>
-              <p class="mt-1"><strong>RGB (3-channel):</strong> WS2811, WS2812, WS2812B, WS2813, WS2815 and other WS281x strips.</p>
-              <p class="mt-1"><strong>RGBW (4-channel):</strong> SK6812, SK6812W and other RGBW strips with dedicated white channel.</p>
-            </div>
-          </div>
-        </div>
-
-        <label class="flex flex-col gap-1.5">
-          <span class="text-slate-700 text-sm font-medium leading-normal">Number of LEDs</span>
-          <input
-            id="dwLedNumLeds"
-            type="number"
-            class="form-input flex w-full min-w-0 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-normal leading-normal transition-colors"
-            placeholder="60"
-            value="60"
-            min="1"
-            max="1000"
-          />
-          <p class="text-xs text-slate-500">
-            Total number of LEDs in your WS281x strip
-          </p>
-        </label>
-        <label class="flex flex-col gap-1.5">
-          <span class="text-slate-700 text-sm font-medium leading-normal">GPIO Pin</span>
-          <select
-            id="dwLedGpioPin"
-            class="form-select flex w-full min-w-0 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 px-4 text-base font-normal leading-normal transition-colors"
-          >
-            <option value="12">GPIO 12 (PWM0)</option>
-            <option value="13">GPIO 13 (PWM1)</option>
-            <option value="18">GPIO 18 (PWM0)</option>
-            <option value="19">GPIO 19 (PWM1)</option>
-          </select>
-          <p class="text-xs text-slate-500">
-            Select a PWM-capable GPIO pin for WS281x timing
-          </p>
-        </label>
-        <label class="flex flex-col gap-1.5">
-          <span class="text-slate-700 text-sm font-medium leading-normal">Pixel Color Order</span>
-          <select
-            id="dwLedPixelOrder"
-            class="form-select flex w-full min-w-0 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 px-4 text-base font-normal leading-normal transition-colors"
-          >
-            <optgroup label="RGB Strips (3-channel)">
-              <option value="GRB" selected>GRB - WS2812/WS2812B (most common)</option>
-              <option value="RGB">RGB - WS2815/WS2811 and some variants</option>
-              <option value="BGR">BGR - Some WS2811 variants</option>
-              <option value="RBG">RBG - Rare variant</option>
-              <option value="GBR">GBR - Rare variant</option>
-              <option value="BRG">BRG - Rare variant</option>
-            </optgroup>
-            <optgroup label="RGBW Strips (4-channel)">
-              <option value="GRBW">GRBW - SK6812 RGBW (most common)</option>
-              <option value="RGBW">RGBW - SK6812 RGBW variant</option>
-            </optgroup>
-          </select>
-          <p class="text-xs text-slate-500">
-            Most WS2812B strips use GRB. SK6812 RGBW strips typically use GRBW. If colors appear wrong, try different orders.
-          </p>
-        </label>
-      </div>
-
-      <!-- Save Button -->
-      <button
-        id="saveLedConfig"
-        class="flex items-center justify-center gap-2 w-full sm:w-auto 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 LED Configuration</span>
-      </button>
-    </div>
-  </section>
-
-  <!-- MQTT Configuration Section -->
-  <section 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>Home Assistant Integration</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">
-      <!-- MQTT Enable Toggle -->
-      <div class="flex items-center justify-between">
-        <div class="flex-1">
-          <h3 class="text-slate-700 text-base font-medium leading-normal">Enable MQTT</h3>
-          <p class="text-xs text-slate-500 mt-1">
-            Connect to an MQTT broker for Home Assistant integration and remote control.
-          </p>
-        </div>
-        <label class="switch">
-          <input type="checkbox" id="mqttEnableToggle">
-          <span class="slider round"></span>
-        </label>
-      </div>
-
-      <!-- Connection Status -->
-      <div id="mqttStatusBanner" class="hidden">
-        <div id="mqttConnectedBanner" class="bg-green-50 border border-green-200 rounded-lg p-3 hidden">
-          <div class="flex items-center gap-2">
-            <span class="material-icons text-green-600 text-base">check_circle</span>
-            <span class="text-sm text-green-700 font-medium">Connected to MQTT broker</span>
-          </div>
-        </div>
-        <div id="mqttDisconnectedBanner" class="bg-amber-50 border border-amber-200 rounded-lg p-3 hidden">
-          <div class="flex items-center gap-2">
-            <span class="material-icons text-amber-600 text-base">warning</span>
-            <span class="text-sm text-amber-700 font-medium">MQTT is enabled but not connected. Check your settings or restart the application.</span>
-          </div>
-        </div>
-      </div>
-
-      <!-- MQTT Settings (shown when enabled) -->
-      <div id="mqttSettings" class="space-y-4" style="display: none;">
-        <!-- Broker Settings -->
-        <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
-          <label class="flex flex-col gap-1.5">
-            <span class="text-slate-700 text-sm font-medium leading-normal">Broker Address <span class="text-red-500">*</span></span>
-            <input
-              id="mqttBrokerInput"
-              type="text"
-              class="form-input flex w-full min-w-0 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-normal leading-normal transition-colors"
-              placeholder="e.g., 192.168.1.100 or mqtt.local"
-            />
-            <p class="text-xs text-slate-500">IP address or hostname of your MQTT broker</p>
-          </label>
-
-          <label class="flex flex-col gap-1.5">
-            <span class="text-slate-700 text-sm font-medium leading-normal">Port</span>
-            <input
-              id="mqttPortInput"
-              type="number"
-              class="form-input flex w-full min-w-0 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-normal leading-normal transition-colors"
-              placeholder="1883"
-              value="1883"
-            />
-            <p class="text-xs text-slate-500">Default: 1883 (or 8883 for TLS)</p>
-          </label>
-        </div>
-
-        <!-- Authentication -->
-        <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
-          <label class="flex flex-col gap-1.5">
-            <span class="text-slate-700 text-sm font-medium leading-normal">Username</span>
-            <input
-              id="mqttUsernameInput"
-              type="text"
-              class="form-input flex w-full min-w-0 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-normal leading-normal transition-colors"
-              placeholder="Optional"
-            />
-            <p class="text-xs text-slate-500">Leave empty if no authentication required</p>
-          </label>
-
-          <label class="flex flex-col gap-1.5">
-            <span class="text-slate-700 text-sm font-medium leading-normal">Password</span>
-            <div class="relative">
-              <input
-                id="mqttPasswordInput"
-                type="password"
-                class="form-input flex w-full min-w-0 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 pr-10 text-base font-normal leading-normal transition-colors"
-                placeholder="Optional"
-              />
-              <button
-                type="button"
-                onclick="togglePasswordVisibility('mqttPasswordInput', this)"
-                class="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-700"
-                aria-label="Toggle password visibility"
-              >
-                <span class="material-icons text-xl">visibility_off</span>
-              </button>
-            </div>
-            <p class="text-xs text-slate-500">Leave empty if no authentication required</p>
-          </label>
-        </div>
-
-        <!-- Home Assistant Discovery Settings -->
-        <div class="border-t border-slate-200 pt-4">
-          <h4 class="text-slate-700 text-sm font-medium mb-3">Home Assistant Discovery</h4>
-          <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
-            <label class="flex flex-col gap-1.5">
-              <span class="text-slate-700 text-sm font-medium leading-normal">Device Name</span>
-              <input
-                id="mqttDeviceNameInput"
-                type="text"
-                class="form-input flex w-full min-w-0 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-normal leading-normal transition-colors"
-                placeholder="Dune Weaver"
-                value="Dune Weaver"
-              />
-              <p class="text-xs text-slate-500">Display name in Home Assistant</p>
-            </label>
-
-            <label class="flex flex-col gap-1.5">
-              <span class="text-slate-700 text-sm font-medium leading-normal">Device ID</span>
-              <input
-                id="mqttDeviceIdInput"
-                type="text"
-                class="form-input flex w-full min-w-0 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-normal leading-normal transition-colors"
-                placeholder="dune_weaver"
-                value="dune_weaver"
-              />
-              <p class="text-xs text-slate-500">Must be unique per table (no spaces). Used for MQTT topics.</p>
-            </label>
-          </div>
-        </div>
-
-        <!-- Client ID and Discovery Prefix -->
-        <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
-          <label class="flex flex-col gap-1.5">
-            <span class="text-slate-700 text-sm font-medium leading-normal">Client ID</span>
-            <input
-              id="mqttClientIdInput"
-              type="text"
-              class="form-input flex w-full min-w-0 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-normal leading-normal transition-colors"
-              placeholder="dune_weaver"
-              value="dune_weaver"
-            />
-            <p class="text-xs text-slate-500">Must be unique if running multiple tables on the same broker</p>
-          </label>
-
-          <label class="flex flex-col gap-1.5">
-            <span class="text-slate-700 text-sm font-medium leading-normal">Discovery Prefix</span>
-            <input
-              id="mqttDiscoveryPrefixInput"
-              type="text"
-              class="form-input flex w-full min-w-0 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-normal leading-normal transition-colors"
-              placeholder="homeassistant"
-              value="homeassistant"
-            />
-            <p class="text-xs text-slate-500">Home Assistant discovery topic prefix</p>
-          </label>
-        </div>
-
-        <!-- Test Connection Button -->
-        <div class="flex flex-wrap gap-3 pt-2">
-          <button
-            id="testMqttConnection"
-            class="flex items-center justify-center gap-2 cursor-pointer rounded-lg h-10 px-4 bg-slate-100 hover:bg-slate-200 text-slate-700 text-sm font-medium leading-normal tracking-[0.015em] transition-colors"
-          >
-            <span class="material-icons text-lg">wifi_tethering</span>
-            <span class="truncate">Test Connection</span>
-          </button>
-          <span id="mqttTestResult" class="flex items-center text-sm"></span>
-        </div>
-      </div>
-
-      <!-- Save Button -->
-      <button
-        id="saveMqttConfig"
-        class="flex items-center justify-center gap-2 w-full sm:w-auto 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 MQTT Configuration</span>
-      </button>
-
-      <!-- Restart Notice -->
-      <div id="mqttRestartNotice" class="bg-blue-50 border border-blue-200 rounded-lg p-3 hidden">
-        <div class="flex items-start gap-2">
-          <span class="material-icons text-blue-600 text-base">info</span>
-          <div class="text-xs text-blue-700">
-            <p class="font-medium text-blue-800">Restart Required</p>
-            <p class="mt-1">MQTT configuration changes require a restart to take effect. Use the restart button in the header to apply changes.</p>
-          </div>
-        </div>
-      </div>
-    </div>
-  </section>
-
-  <section 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>Auto-play on Boot</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">
-      <div class="flex items-center justify-between">
-        <div class="flex-1">
-          <h3 class="text-slate-700 text-base font-medium leading-normal">Enable Auto-play on Boot</h3>
-          <p class="text-xs text-slate-500 mt-1">
-            Automatically start playing a selected playlist when the system boots up.
-          </p>
-        </div>
-        <label class="switch">
-          <input type="checkbox" id="auto_playModeToggle">
-          <span class="slider round"></span>
-        </label>
-      </div>
-      
-      <div id="auto_playSettings" class="space-y-4" style="display: none;">
-        <label class="flex flex-col gap-1.5">
-          <span class="text-slate-700 text-sm font-medium leading-normal">Startup Playlist</span>
-          <select
-            id="auto_playPlaylistSelect"
-            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="">Select a playlist...</option>
-          </select>
-          <p class="text-xs text-slate-500 mt-1">
-            Choose which playlist to automatically play when the system starts.
-          </p>
-        </label>
-
-        <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
-          <label class="flex flex-col gap-1.5">
-            <span class="text-slate-700 text-sm font-medium leading-normal">Run Mode</span>
-            <select
-              id="auto_playRunModeSelect"
-              class="form-select 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="single">Single (play once)</option>
-              <option value="loop">Loop (repeat forever)</option>
-            </select>
-            <p class="text-xs text-slate-500 mt-1">
-              How to run the playlist when it finishes.
-            </p>
-          </label>
-
-          <label class="flex flex-col gap-1.5">
-            <span class="text-slate-700 text-sm font-medium leading-normal">Pause Between Patterns (seconds)</span>
-            <input
-              id="auto_playPauseTimeInput"
-              type="number"
-              min="0"
-              step="0.5"
-              class="form-input 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-normal leading-normal transition-colors"
-              placeholder="5.0"
-            />
-            <p class="text-xs text-slate-500 mt-1">
-              Time to wait between each pattern (0 or more seconds).
-            </p>
-          </label>
-        </div>
-
-        <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
-          <label class="flex flex-col gap-1.5">
-            <span class="text-slate-700 text-sm font-medium leading-normal">Clear Pattern</span>
-            <select
-              id="auto_playClearPatternSelect"
-              class="form-select 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="none">None</option>
-              <option value="adaptive">Adaptive</option>
-              <option value="clear_from_in">Clear From Center</option>
-              <option value="clear_from_out">Clear From Perimeter</option>
-              <option value="clear_sideway">Clear Sideway</option>
-              <option value="random">Random</option>
-            </select>
-            <p class="text-xs text-slate-500 mt-1">
-              Pattern to run before each main pattern.
-            </p>
-          </label>
-
-          <div class="flex items-center justify-between">
-            <div class="flex-1">
-              <h4 class="text-slate-700 text-sm font-medium leading-normal">Shuffle Playlist</h4>
-              <p class="text-xs text-slate-500 mt-1">
-                Randomize the order of patterns in the playlist.
-              </p>
-            </div>
-            <label class="switch">
-              <input type="checkbox" id="auto_playShuffleToggle">
-              <span class="slider round"></span>
-            </label>
-          </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>
-  </section>
-  <section 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>Still Sands</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">
-      <div class="flex items-center justify-between">
-        <div class="flex-1">
-          <h3 class="text-slate-700 text-base font-medium leading-normal">Enable Still Sands</h3>
-          <p class="text-xs text-slate-500 mt-1">
-            Automatically bring the sands to rest during specified time periods.
-          </p>
-        </div>
-        <label class="switch">
-          <input type="checkbox" id="scheduledPauseToggle">
-          <span class="slider round"></span>
-        </label>
-      </div>
-
-      <div id="scheduledPauseSettings" class="space-y-4" style="display: none;">
-        <!-- Finish Current Pattern Option -->
-        <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">hourglass_bottom</span>
-                Finish Current Pattern
-              </h4>
-              <p class="text-xs text-slate-600 mt-1">
-                Let the current pattern complete before entering still mode
-              </p>
-            </div>
-            <label class="switch">
-              <input type="checkbox" id="stillSandsFinishPattern">
-              <span class="slider round"></span>
-            </label>
-          </div>
-        </div>
-
-        <!-- WLED Control Option -->
-        <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">lightbulb</span>
-                Control WLED Lights
-              </h4>
-              <p class="text-xs text-slate-600 mt-1">
-                Turn off WLED lights during still periods for complete rest
-              </p>
-            </div>
-            <label class="switch">
-              <input type="checkbox" id="stillSandsWledControl">
-              <span class="slider round"></span>
-            </label>
-          </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="flex items-center justify-between">
-            <h4 class="text-slate-800 text-base font-semibold">Still Periods</h4>
-            <button
-              id="addTimeSlotButton"
-              class="flex items-center justify-center gap-2 cursor-pointer rounded-lg h-9 px-3 bg-sky-600 hover:bg-sky-700 text-white text-xs font-medium leading-normal tracking-[0.015em] transition-colors"
-            >
-              <span class="material-icons text-base">add</span>
-              <span>Add Still Period</span>
-            </button>
-          </div>
-          <p class="text-sm text-slate-600">
-            Define time periods when the sands should rest in stillness. Patterns will resume automatically when still periods end.
-          </p>
-
-          <div id="timeSlotsContainer" class="space-y-3">
-            <!-- Time slots will be dynamically added here -->
-          </div>
-
-          <div class="text-xs text-slate-500 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">Important Notes:</p>
-                <ul class="mt-1 space-y-1 text-blue-700">
-                  <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>• Enable "Finish Current Pattern" to let patterns complete first</li>
-                  <li>• Patterns will resume automatically when exiting a still period</li>
-                  <li>• Still periods that span midnight (e.g., 22:00 to 06:00) are supported</li>
-                </ul>
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <div class="flex justify-end">
-          <button
-            id="savePauseSettings"
-            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 Still Sands</span>
-          </button>
-        </div>
-      </div>
-    </div>
-  </section>
-  <section id="software-version-section" 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>Software Version</span>
-      <span class="material-icons section-toggle-icon text-slate-400">expand_more</span>
-    </h2>
-    <div class="section-content collapsed">
-      <div class="flex items-center gap-4 px-6 py-5">
-        <div
-          class="text-slate-600 flex items-center justify-center rounded-lg bg-slate-100 shrink-0 size-12"
-        >
-          <span class="material-icons text-3xl">terminal</span>
-        </div>
-        <div class="flex-1">
-          <p class="text-slate-800 text-base font-medium leading-normal">
-            Current Version
-          </p>
-          <p id="currentVersionText" class="text-slate-500 text-sm font-normal leading-normal">Loading...</p>
-        </div>
-      </div>
-      <div class="flex items-center gap-4 px-6 py-5">
-        <div
-          class="text-slate-600 flex items-center justify-center rounded-lg bg-slate-100 shrink-0 size-12"
-        >
-          <span class="material-icons text-3xl">system_update</span>
-        </div>
-        <div class="flex-1">
-          <p class="text-slate-800 text-base font-medium leading-normal">
-            Latest Version
-          </p>
-          <p id="latestVersionText" class="text-slate-500 text-sm font-normal leading-normal">Checking...</p>
-        </div>
-        <button
-          id="updateSoftware"
-          class="flex items-center justify-center gap-1.5 min-w-[84px] cursor-pointer rounded-lg h-9 px-3 bg-gray-400 text-white text-xs font-medium leading-normal tracking-[0.015em] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
-          disabled
-        >
-          <span id="updateIcon" class="material-icons text-base">download</span>
-          <span id="updateText" class="truncate">Update</span>
-        </button>
-      </div>
-      <div class="flex items-center gap-4 px-6 py-5 border-t border-slate-200">
-        <div
-          class="text-slate-600 flex items-center justify-center rounded-lg bg-slate-100 shrink-0 size-12"
-        >
-          <span class="material-icons text-3xl">article</span>
-        </div>
-        <div class="flex-1">
-          <p class="text-slate-800 text-base font-medium leading-normal">
-            Application Logs
-          </p>
-          <p class="text-slate-500 text-sm font-normal leading-normal">View real-time application logs</p>
-        </div>
-        <button
-          id="openLogsBtn"
-          onclick="openLogsModal()"
-          class="flex items-center justify-center gap-1.5 min-w-[84px] cursor-pointer rounded-lg h-9 px-3 bg-blue-600 hover:bg-blue-700 text-white text-xs font-medium leading-normal tracking-[0.015em] transition-colors"
-        >
-          <span class="material-icons text-base">terminal</span>
-          <span class="truncate">View Logs</span>
-        </button>
-      </div>
-    </div>
-  </section>
-</div>
-{% endblock %} {% block scripts %}
-<script src="/static/js/settings.js"></script>
-{% endblock %}

+ 0 - 354
templates/table_control.html

@@ -1,354 +0,0 @@
-{% extends "base.html" %}
-{% block title %}Table Control - {{ app_name or 'Dune Weaver' }}{% endblock %}
-
-{% block additional_styles %}
-/* Dark mode styles for table control page */
-.dark .bg-white {
-  background-color: #262626;
-}
-.dark .text-slate-900 {
-  color: #e5e5e5;
-}
-.dark .text-slate-800 {
-  color: #e5e5e5;
-}
-.dark .text-slate-700 {
-  color: #d1d5db;
-}
-.dark .border-slate-200 {
-  border-color: #404040;
-}
-.dark .border-slate-300 {
-  border-color: #404040;
-}
-.dark .bg-slate-50 {
-  background-color: #262626;
-}
-.dark .hover\:bg-slate-50:hover {
-  background-color: #404040;
-}
-.dark .form-input {
-  background-color: #262626;
-  border-color: #404040;
-  color: #e5e5e5;
-}
-.dark .form-input::placeholder {
-  color: #9ca3af;
-}
-.dark .form-input:focus {
-  border-color: #0c7ff2;
-  ring-color: #0c7ff2;
-}
-.dark .focus\:ring-sky-500:focus {
-  ring-color: #0c7ff2;
-}
-.dark .focus\:border-sky-500:focus {
-  border-color: #0c7ff2;
-}
-.dark .focus\:ring-blue-400:focus {
-  ring-color: #0c7ff2;
-}
-.dark .focus\:ring-red-400:focus {
-  ring-color: #ef4444;
-}
-.dark .focus\:ring-slate-400:focus {
-  ring-color: #525252;
-}
-.dark .focus\:ring-offset-2 {
-  ring-offset-color: #262626;
-}
-{% endblock %}
-
-{% block content %}
-<div class="layout-content-container flex flex-col w-full max-w-4xl gap-8 pt-2 pb-[75px]">
-  <div class="flex flex-wrap justify-between items-center gap-4 p-4 bg-white rounded-xl shadow-sm mt-2 sm:mt-8">
-    <h1 class="text-slate-900 tracking-tight text-2xl sm:text-3xl font-bold leading-tight">
-      Table Control
-    </h1>
-  </div>
-
-  <section class="bg-white p-6 rounded-xl shadow-lg">
-    <h2 class="text-xl font-semibold text-slate-800 mb-5 border-b border-slate-200 pb-3">
-      Movement Controls
-    </h2>
-    <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
-      <button
-        id="homeButton"
-        class="flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-3 text-sm font-semibold text-white shadow-md hover:bg-blue-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2"
-      >
-        <span class="material-icons-outlined text-lg">home</span>
-        Home
-      </button>
-      <button
-        id="stopButton"
-        class="flex items-center justify-center gap-2 rounded-lg bg-red-500 px-4 py-3 text-sm font-semibold text-white shadow-md hover:bg-red-600 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-red-400 focus:ring-offset-2"
-      >
-        <span class="material-icons-outlined text-lg">stop_circle</span>
-        Stop
-      </button>
-      <button
-        id="centerButton"
-        class="flex items-center justify-center gap-2 rounded-lg bg-slate-600 px-4 py-3 text-sm font-semibold text-white shadow-md hover:bg-slate-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2"
-      >
-        <span class="material-icons-outlined text-lg">filter_center_focus</span>
-        Move to Center
-      </button>
-      <button
-        id="perimeterButton"
-        class="flex items-center justify-center gap-2 rounded-lg bg-slate-600 px-4 py-3 text-sm font-semibold text-white shadow-md hover:bg-slate-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2"
-      >
-        <span class="material-icons-outlined text-lg">all_out</span>
-        Move to Perimeter
-      </button>
-    </div>
-    <div class="flex items-center justify-center mt-4">
-      <button
-        id="orientationHelpButton"
-        class="flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1"
-      >
-        <span class="material-icons-outlined text-lg">help_outline</span>
-        How to Align Pattern Orientation
-      </button>
-    </div>
-  </section>
-
-  <section class="bg-white p-6 rounded-xl shadow-lg">
-    <h2 class="text-xl font-semibold text-slate-800 mb-5 border-b border-slate-200 pb-3">
-      Speed Control
-    </h2>
-    <div class="flex flex-col sm:flex-row items-start sm:items-center gap-4">
-      <div class="flex items-center gap-2 bg-slate-50 rounded-lg px-3 py-2 w-full sm:w-auto">
-        <span class="text-sm font-medium text-slate-700">Current:</span>
-        <span id="currentSpeedDisplay" class="text-sm font-semibold text-slate-900">-- mm/s</span>
-      </div>
-      
-      <div class="flex flex-col sm:flex-row gap-3 w-full sm:flex-1">
-        <div class="flex-1">
-          <input
-            type="number"
-            id="speedInput"
-            class="form-input flex w-full min-w-0 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-3 text-sm font-normal leading-normal transition-colors"
-            placeholder="Enter new speed..."
-            min="1"
-            step="1"
-          />
-        </div>
-        <button
-          id="setSpeedButton"
-          class="flex items-center justify-center gap-2 rounded-lg bg-sky-600 px-4 py-2.5 text-sm font-semibold text-white shadow-md hover:bg-sky-700 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-sky-400 focus:ring-offset-2 h-10 w-full sm:w-auto whitespace-nowrap"
-        >
-          <span class="material-icons-outlined text-lg">speed</span>
-          Set Speed
-        </button>
-      </div>
-    </div>
-  </section>
-
-  <section class="bg-white p-6 rounded-xl shadow-lg">
-    <h2 class="text-xl font-semibold text-slate-800 mb-5 border-b border-slate-200 pb-3">
-      Clear Patterns
-    </h2>
-    <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
-      <button
-        id="clearCenterButton"
-        class="flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1"
-      >
-        <span class="material-icons-outlined text-lg">center_focus_strong</span>
-        Clear from Center
-      </button>
-      <button
-        id="clearPerimeterButton"
-        class="flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1"
-      >
-        <span class="material-icons-outlined text-lg">all_out</span>
-        Clear from Perimeter
-      </button>
-      <button
-        id="clearSidewaysButton"
-        class="flex items-center justify-center gap-2 rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 transition-colors duration-150 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-1"
-      >
-        <span class="material-icons-outlined text-lg">swap_horiz</span>
-        Clear Sideway
-      </button>
-    </div>
-  </section>
-</div>
-
-<!-- Pattern Orientation Help Modal -->
-<div id="orientationHelpModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden p-4">
-  <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md">
-    <div class="p-6">
-      <div class="text-center mb-4">
-        <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-2">Pattern Orientation Alignment</h2>
-        <p class="text-gray-600 dark:text-gray-400 text-sm">
-          Follow these steps to align your patterns with their previews:
-        </p>
-      </div>
-      
-      <ol class="text-gray-700 dark:text-gray-300 text-sm space-y-3 mb-6">
-        <li class="flex items-start gap-3">
-          <span class="font-semibold text-blue-600 dark:text-blue-400 min-w-[20px]">1.</span>
-          <span><strong>Home the table</strong> then select move to perimeter. Look at your pattern preview and decide where the "bottom" of the pattern should be.</span>
-        </li>
-        <li class="flex items-start gap-3">
-          <span class="font-semibold text-blue-600 dark:text-blue-400 min-w-[20px]">2.</span>
-          <span><strong>Manually</strong> move the radial arm or <strong>use the rotation buttons below</strong> to point 90° to the right of where you want the pattern bottom to be.</span>
-        </li>
-        <li class="flex items-start gap-3">
-          <span class="font-semibold text-blue-600 dark:text-blue-400 min-w-[20px]">3.</span>
-          <span>Click the <strong>"Home"</strong> button to establish this as the reference position.</span>
-        </li>
-        <li class="flex items-start gap-3">
-          <span class="font-semibold text-blue-600 dark:text-blue-400 min-w-[20px]">4.</span>
-          <span>All patterns will now be oriented according to their previews!</span>
-        </li>
-      </ol>
-      
-      <div class="border-t border-gray-200 dark:border-gray-600 pt-4 mb-4">
-        <p class="text-amber-600 dark:text-amber-400 text-sm">
-          <strong>Important:</strong> Only perform this alignment when you want to change the orientation reference. Once set, this becomes your new "home" position.
-        </p>
-      </div>
-      
-      <!-- Fine Adjustment Controls -->
-      <div class="border-t border-gray-200 dark:border-gray-600 pt-4 mb-4">
-        <p class="text-gray-700 dark:text-gray-300 text-sm mb-3 text-center">
-          <strong>Fine Adjustment:</strong> Use these buttons to rotate the ball precisely
-        </p>
-        <div class="flex justify-center gap-3">
-          <button
-            id="rotateCCW"
-            class="flex items-center gap-2 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors"
-            title="Rotate Counter-Clockwise 10°"
-          >
-            <span class="material-icons text-lg">rotate_left</span>
-            <span class="text-sm">CCW 10°</span>
-          </button>
-          <button
-            id="rotateCW"
-            class="flex items-center gap-2 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors"
-            title="Rotate Clockwise 10°"
-          >
-            <span class="text-sm">CW 10°</span>
-            <span class="material-icons text-lg">rotate_right</span>
-          </button>
-        </div>
-        <p class="text-gray-500 dark:text-gray-400 text-xs text-center mt-2">
-          Each click rotates 10 degrees for fine adjustment
-        </p>
-      </div>
-      
-      <div class="flex justify-center">
-        <button
-          id="closeOrientationHelpModal"
-          class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors"
-        >
-          Got it
-        </button>
-      </div>
-    </div>
-  </div>
-</div>
-{% endblock %}
-
-{% block scripts %}
-<script src="/static/js/table_control.js"></script>
-<script>
-// Pattern orientation help modal functionality
-document.addEventListener('DOMContentLoaded', function() {
-  const helpButton = document.getElementById('orientationHelpButton');
-  const modal = document.getElementById('orientationHelpModal');
-  const closeButton = document.getElementById('closeOrientationHelpModal');
-  const rotateCWButton = document.getElementById('rotateCW');
-  const rotateCCWButton = document.getElementById('rotateCCW');
-  
-  // Track current position (theta, rho)
-  let currentTheta = 0;
-  let currentRho = 1; // Always at perimeter for rotation adjustments
-  
-  helpButton.addEventListener('click', () => {
-    modal.classList.remove('hidden');
-  });
-  
-  closeButton.addEventListener('click', () => {
-    modal.classList.add('hidden');
-  });
-  
-  // Close modal when clicking outside
-  modal.addEventListener('click', (e) => {
-    if (e.target === modal) {
-      modal.classList.add('hidden');
-    }
-  });
-  
-  // Rotation button handlers
-  async function rotateByDegrees(degrees) {
-    try {
-      // Convert degrees to radians
-      const radians = degrees * (Math.PI / 180);
-      
-      // Calculate new theta position
-      const newTheta = currentTheta + radians;
-      
-      // Send coordinate to move to new position (always at perimeter, rho = 1)
-      const response = await fetch('/send_coordinate', {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-        },
-        body: JSON.stringify({
-          theta: newTheta,
-          rho: 1
-        })
-      });
-      
-      if (response.ok) {
-        // Update tracked position
-        currentTheta = newTheta;
-        console.log(`Rotated ${degrees}°. New theta: ${(currentTheta * 180 / Math.PI).toFixed(1)}°`);
-      } else {
-        throw new Error('Failed to send coordinate');
-      }
-    } catch (error) {
-      console.error('Error rotating:', error);
-      alert('Failed to rotate. Please check connection.');
-    }
-  }
-  
-  // Clockwise rotation (positive angle)
-  rotateCWButton.addEventListener('click', async () => {
-    await rotateByDegrees(10);
-  });
-  
-  // Counter-clockwise rotation (negative angle)  
-  rotateCCWButton.addEventListener('click', async () => {
-    await rotateByDegrees(-10);
-  });
-  
-  // Try to get current position from WebSocket status
-  // This assumes you have a WebSocket connection for status updates
-  if (typeof ws !== 'undefined' && ws) {
-    const originalOnMessage = ws.onmessage;
-    ws.onmessage = function(event) {
-      try {
-        const data = JSON.parse(event.data);
-        if (data.type === 'status_update' && data.data) {
-          // Update current theta position if available in status
-          if (data.data.current_theta !== undefined) {
-            currentTheta = data.data.current_theta;
-          }
-          // Keep rho at 1 for rotation adjustments
-          currentRho = 1;
-        }
-      } catch (error) {
-        // Ignore parsing errors
-      }
-      // Call original handler if it exists
-      if (originalOnMessage) {
-        originalOnMessage.call(ws, event);
-      }
-    };
-  }
-});
-</script>
-{% endblock %} 

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor