settings.js 100 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439
  1. // ============================================================================
  2. // Collapsible Section Toggle
  3. // ============================================================================
  4. function toggleSection(headerElement) {
  5. const contentElement = headerElement.nextElementSibling;
  6. if (headerElement.classList.contains('collapsed')) {
  7. // Expand
  8. headerElement.classList.remove('collapsed');
  9. contentElement.classList.remove('collapsed');
  10. } else {
  11. // Collapse
  12. headerElement.classList.add('collapsed');
  13. contentElement.classList.add('collapsed');
  14. }
  15. }
  16. // Initialize section headers with proper event listeners for cross-browser compatibility
  17. // This fixes Firefox issues where inline onclick handlers may not work reliably
  18. // with user-select: none and flexbox layouts
  19. function initializeSectionHeaders() {
  20. const sectionHeaders = document.querySelectorAll('.section-header');
  21. sectionHeaders.forEach(header => {
  22. // Remove inline onclick to prevent double-firing
  23. header.removeAttribute('onclick');
  24. // Add click event listener (more reliable than inline onclick in Firefox)
  25. header.addEventListener('click', function(e) {
  26. // Prevent text selection on double-click
  27. e.preventDefault();
  28. toggleSection(this);
  29. });
  30. // Also handle keyboard accessibility
  31. header.setAttribute('role', 'button');
  32. header.setAttribute('tabindex', '0');
  33. header.addEventListener('keydown', function(e) {
  34. if (e.key === 'Enter' || e.key === ' ') {
  35. e.preventDefault();
  36. toggleSection(this);
  37. }
  38. });
  39. });
  40. }
  41. // Initialize on DOM ready
  42. document.addEventListener('DOMContentLoaded', initializeSectionHeaders);
  43. // ============================================================================
  44. // Constants and Utilities
  45. // ============================================================================
  46. // Constants for log message types
  47. const LOG_TYPE = {
  48. SUCCESS: 'success',
  49. WARNING: 'warning',
  50. ERROR: 'error',
  51. INFO: 'info',
  52. DEBUG: 'debug'
  53. };
  54. // Helper function to convert provider name to camelCase for ID lookup
  55. // e.g., "dw_leds" -> "DwLeds", "wled" -> "Wled", "none" -> "None"
  56. function providerToCamelCase(provider) {
  57. return provider.split('_').map(word =>
  58. word.charAt(0).toUpperCase() + word.slice(1)
  59. ).join('');
  60. }
  61. // Constants for cache
  62. const CACHE_KEYS = {
  63. CONNECTION_STATUS: 'connection_status',
  64. LAST_UPDATE: 'last_status_update'
  65. };
  66. const CACHE_DURATION = 5000; // 5 seconds cache duration
  67. // Function to log messages
  68. function logMessage(message, type = LOG_TYPE.DEBUG) {
  69. console.log(`[${type}] ${message}`);
  70. }
  71. // Function to get cached connection status
  72. function getCachedConnectionStatus() {
  73. const cachedData = localStorage.getItem(CACHE_KEYS.CONNECTION_STATUS);
  74. const lastUpdate = localStorage.getItem(CACHE_KEYS.LAST_UPDATE);
  75. if (cachedData && lastUpdate) {
  76. const now = Date.now();
  77. const cacheAge = now - parseInt(lastUpdate);
  78. if (cacheAge < CACHE_DURATION) {
  79. return JSON.parse(cachedData);
  80. }
  81. }
  82. return null;
  83. }
  84. // Function to set cached connection status
  85. function setCachedConnectionStatus(data) {
  86. localStorage.setItem(CACHE_KEYS.CONNECTION_STATUS, JSON.stringify(data));
  87. localStorage.setItem(CACHE_KEYS.LAST_UPDATE, Date.now().toString());
  88. }
  89. // Function to update serial connection status
  90. async function updateSerialStatus(forceUpdate = false) {
  91. try {
  92. // Check cache first unless force update is requested
  93. if (!forceUpdate) {
  94. const cachedData = getCachedConnectionStatus();
  95. if (cachedData) {
  96. updateConnectionUI(cachedData);
  97. return;
  98. }
  99. }
  100. const response = await fetch('/serial_status');
  101. if (response.ok) {
  102. const data = await response.json();
  103. setCachedConnectionStatus(data);
  104. updateConnectionUI(data);
  105. }
  106. } catch (error) {
  107. logMessage(`Error checking serial status: ${error.message}`, LOG_TYPE.ERROR);
  108. }
  109. }
  110. // Function to update UI based on connection status
  111. function updateConnectionUI(data) {
  112. const statusElement = document.getElementById('serialStatus');
  113. const iconElement = document.querySelector('.material-icons.text-3xl');
  114. const disconnectButton = document.getElementById('disconnectButton');
  115. const portSelectionDiv = document.getElementById('portSelectionDiv');
  116. if (statusElement && iconElement) {
  117. if (data.connected) {
  118. statusElement.textContent = `Connected to ${data.port || 'unknown port'}`;
  119. statusElement.className = 'text-green-500 text-sm font-medium leading-normal';
  120. iconElement.textContent = 'usb';
  121. if (disconnectButton) {
  122. disconnectButton.hidden = false;
  123. }
  124. if (portSelectionDiv) {
  125. portSelectionDiv.hidden = true;
  126. }
  127. } else {
  128. statusElement.textContent = 'Disconnected';
  129. statusElement.className = 'text-red-500 text-sm font-medium leading-normal';
  130. iconElement.textContent = 'usb_off';
  131. if (disconnectButton) {
  132. disconnectButton.hidden = true;
  133. }
  134. if (portSelectionDiv) {
  135. portSelectionDiv.hidden = false;
  136. }
  137. }
  138. }
  139. }
  140. // Function to update available serial ports
  141. async function updateSerialPorts() {
  142. try {
  143. const response = await fetch('/list_serial_ports');
  144. if (response.ok) {
  145. const ports = await response.json();
  146. const portsElement = document.getElementById('availablePorts');
  147. const portSelect = document.getElementById('portSelect');
  148. const preferredPortSelect = document.getElementById('preferredPortSelect');
  149. if (portsElement) {
  150. portsElement.textContent = ports.length > 0 ? ports.join(', ') : 'No ports available';
  151. }
  152. if (portSelect) {
  153. // Clear existing options except the first one
  154. while (portSelect.options.length > 1) {
  155. portSelect.remove(1);
  156. }
  157. // Add new options
  158. ports.forEach(port => {
  159. const option = document.createElement('option');
  160. option.value = port;
  161. option.textContent = port;
  162. portSelect.appendChild(option);
  163. });
  164. // If there's exactly one port available, select and connect to it
  165. if (ports.length === 1) {
  166. portSelect.value = ports[0];
  167. // Trigger connect button click
  168. const connectButton = document.getElementById('connectButton');
  169. if (connectButton) {
  170. connectButton.click();
  171. }
  172. }
  173. }
  174. // Also update the preferred port select dropdown
  175. if (preferredPortSelect) {
  176. // Store current selection
  177. const currentPreferred = preferredPortSelect.value;
  178. // Clear existing options except the first one (no preference)
  179. while (preferredPortSelect.options.length > 1) {
  180. preferredPortSelect.remove(1);
  181. }
  182. // Add all available ports
  183. ports.forEach(port => {
  184. const option = document.createElement('option');
  185. option.value = port;
  186. option.textContent = port;
  187. preferredPortSelect.appendChild(option);
  188. });
  189. // Restore selection if it's still available
  190. if (currentPreferred && ports.includes(currentPreferred)) {
  191. preferredPortSelect.value = currentPreferred;
  192. }
  193. }
  194. }
  195. } catch (error) {
  196. logMessage(`Error fetching serial ports: ${error.message}`, LOG_TYPE.ERROR);
  197. }
  198. }
  199. // Function to load and display preferred port setting
  200. async function loadPreferredPort() {
  201. try {
  202. const response = await fetch('/api/preferred-port');
  203. if (response.ok) {
  204. const data = await response.json();
  205. const preferredPortSelect = document.getElementById('preferredPortSelect');
  206. const currentPreferredPort = document.getElementById('currentPreferredPort');
  207. const preferredPortDisplay = document.getElementById('preferredPortDisplay');
  208. if (preferredPortSelect && data.preferred_port) {
  209. // Check if the preferred port is in the options
  210. const optionExists = Array.from(preferredPortSelect.options).some(
  211. opt => opt.value === data.preferred_port
  212. );
  213. if (optionExists) {
  214. preferredPortSelect.value = data.preferred_port;
  215. } else {
  216. // Add the preferred port as an option (it might not be currently available)
  217. const option = document.createElement('option');
  218. option.value = data.preferred_port;
  219. option.textContent = `${data.preferred_port} (not currently available)`;
  220. preferredPortSelect.appendChild(option);
  221. preferredPortSelect.value = data.preferred_port;
  222. }
  223. }
  224. // Show current preferred port indicator
  225. if (currentPreferredPort && preferredPortDisplay && data.preferred_port) {
  226. preferredPortDisplay.textContent = `Currently set to: ${data.preferred_port}`;
  227. currentPreferredPort.classList.remove('hidden');
  228. } else if (currentPreferredPort) {
  229. currentPreferredPort.classList.add('hidden');
  230. }
  231. }
  232. } catch (error) {
  233. logMessage(`Error loading preferred port: ${error.message}`, LOG_TYPE.ERROR);
  234. }
  235. }
  236. // Function to save preferred port setting
  237. async function savePreferredPort() {
  238. const preferredPortSelect = document.getElementById('preferredPortSelect');
  239. if (!preferredPortSelect) return;
  240. const preferredPort = preferredPortSelect.value || null;
  241. try {
  242. const response = await fetch('/api/settings', {
  243. method: 'PATCH',
  244. headers: { 'Content-Type': 'application/json' },
  245. body: JSON.stringify({ connection: { preferred_port: preferredPort } })
  246. });
  247. if (response.ok) {
  248. await response.json();
  249. const currentPreferredPort = document.getElementById('currentPreferredPort');
  250. const preferredPortDisplay = document.getElementById('preferredPortDisplay');
  251. if (preferredPort) {
  252. showStatusMessage(`Preferred port set to: ${preferredPort}`, 'success');
  253. if (currentPreferredPort && preferredPortDisplay) {
  254. preferredPortDisplay.textContent = `Currently set to: ${preferredPort}`;
  255. currentPreferredPort.classList.remove('hidden');
  256. }
  257. } else {
  258. showStatusMessage('Preferred port cleared - will auto-detect on startup', 'success');
  259. if (currentPreferredPort) {
  260. currentPreferredPort.classList.add('hidden');
  261. }
  262. }
  263. } else {
  264. throw new Error('Failed to save preferred port');
  265. }
  266. } catch (error) {
  267. showStatusMessage(`Failed to save preferred port: ${error.message}`, 'error');
  268. }
  269. }
  270. function setWledButtonState(isSet) {
  271. const saveWledConfig = document.getElementById('saveWledConfig');
  272. if (!saveWledConfig) return;
  273. if (isSet) {
  274. 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';
  275. saveWledConfig.innerHTML = '<span class="material-icons text-lg">close</span><span class="truncate">Clear WLED IP</span>';
  276. } else {
  277. 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';
  278. saveWledConfig.innerHTML = '<span class="material-icons text-lg">save</span><span class="truncate">Save Configuration</span>';
  279. }
  280. }
  281. // Handle LED provider selection and show/hide appropriate config sections
  282. function updateLedProviderUI() {
  283. const provider = document.querySelector('input[name="ledProvider"]:checked')?.value || 'none';
  284. const wledConfig = document.getElementById('wledConfig');
  285. const dwLedsConfig = document.getElementById('dwLedsConfig');
  286. if (wledConfig && dwLedsConfig) {
  287. if (provider === 'wled') {
  288. wledConfig.classList.remove('hidden');
  289. dwLedsConfig.classList.add('hidden');
  290. } else if (provider === 'dw_leds') {
  291. wledConfig.classList.add('hidden');
  292. dwLedsConfig.classList.remove('hidden');
  293. } else {
  294. wledConfig.classList.add('hidden');
  295. dwLedsConfig.classList.add('hidden');
  296. }
  297. }
  298. }
  299. // Load LED configuration from server
  300. async function loadLedConfig() {
  301. try {
  302. const response = await fetch('/get_led_config');
  303. if (response.ok) {
  304. const data = await response.json();
  305. // Set provider radio button
  306. const providerRadio = document.getElementById(`ledProvider${providerToCamelCase(data.provider)}`);
  307. if (providerRadio) {
  308. providerRadio.checked = true;
  309. } else {
  310. document.getElementById('ledProviderNone').checked = true;
  311. }
  312. // Set WLED IP if configured
  313. if (data.wled_ip) {
  314. const wledIpInput = document.getElementById('wledIpInput');
  315. if (wledIpInput) {
  316. wledIpInput.value = data.wled_ip;
  317. }
  318. }
  319. // Set DW LED configuration if configured
  320. if (data.dw_led_num_leds) {
  321. const numLedsInput = document.getElementById('dwLedNumLeds');
  322. if (numLedsInput) {
  323. numLedsInput.value = data.dw_led_num_leds;
  324. }
  325. }
  326. if (data.dw_led_gpio_pin) {
  327. const gpioPinInput = document.getElementById('dwLedGpioPin');
  328. if (gpioPinInput) {
  329. gpioPinInput.value = data.dw_led_gpio_pin;
  330. }
  331. }
  332. if (data.dw_led_pixel_order) {
  333. const pixelOrderInput = document.getElementById('dwLedPixelOrder');
  334. if (pixelOrderInput) {
  335. pixelOrderInput.value = data.dw_led_pixel_order;
  336. }
  337. }
  338. // Update UI to show correct config section
  339. updateLedProviderUI();
  340. }
  341. } catch (error) {
  342. logMessage(`Error loading LED config: ${error.message}`, LOG_TYPE.ERROR);
  343. }
  344. }
  345. // Initialize settings page
  346. document.addEventListener('DOMContentLoaded', async () => {
  347. // Initialize UI with default disconnected state
  348. updateConnectionUI({ connected: false });
  349. // Handle scroll to section if hash is present in URL
  350. if (window.location.hash) {
  351. setTimeout(() => {
  352. const targetSection = document.querySelector(window.location.hash);
  353. if (targetSection) {
  354. targetSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
  355. // Add a subtle highlight animation
  356. targetSection.style.transition = 'background-color 0.5s ease';
  357. const originalBg = targetSection.style.backgroundColor;
  358. targetSection.style.backgroundColor = 'rgba(14, 165, 233, 0.1)';
  359. setTimeout(() => {
  360. targetSection.style.backgroundColor = originalBg;
  361. }, 2000);
  362. }
  363. }, 300); // Delay to ensure page is fully loaded
  364. }
  365. // Load all data asynchronously using unified settings endpoint
  366. Promise.all([
  367. // Unified settings endpoint (replaces multiple individual fetches)
  368. fetch('/api/settings').then(response => response.json()).catch(() => ({})),
  369. // Non-settings operational endpoints (kept separate)
  370. fetch('/serial_status').then(response => response.json()).catch(() => ({ connected: false })),
  371. fetch('/api/version').then(response => response.json()).catch(() => ({ current: '1.0.0', latest: '1.0.0', update_available: false })),
  372. fetch('/list_serial_ports').then(response => response.json()).catch(() => []),
  373. getCachedPatternFiles().catch(() => [])
  374. ]).then(([settings, statusData, updateData, ports, patterns]) => {
  375. // Map unified settings to legacy variable names for backward compatibility with existing UI code
  376. const ledConfigData = {
  377. provider: settings.led?.provider || 'none',
  378. wled_ip: settings.led?.wled_ip || null,
  379. dw_led_num_leds: settings.led?.dw_led?.num_leds,
  380. dw_led_gpio_pin: settings.led?.dw_led?.gpio_pin,
  381. dw_led_pixel_order: settings.led?.dw_led?.pixel_order
  382. };
  383. const clearPatterns = {
  384. custom_clear_from_in: settings.patterns?.custom_clear_from_in,
  385. custom_clear_from_out: settings.patterns?.custom_clear_from_out
  386. };
  387. const clearSpeedData = {
  388. clear_pattern_speed: settings.patterns?.clear_pattern_speed,
  389. effective_speed: settings.patterns?.clear_pattern_speed // Will be handled by UI
  390. };
  391. const appNameData = { app_name: settings.app?.name || 'Dune Weaver' };
  392. const scheduledPauseData = settings.scheduled_pause || { enabled: false, time_slots: [] };
  393. const preferredPortData = { preferred_port: settings.connection?.preferred_port };
  394. // Store full settings for other initialization functions
  395. window.unifiedSettings = settings;
  396. // Update connection status
  397. setCachedConnectionStatus(statusData);
  398. updateConnectionUI(statusData);
  399. // Update LED configuration
  400. const providerRadio = document.getElementById(`ledProvider${providerToCamelCase(ledConfigData.provider)}`);
  401. if (providerRadio) {
  402. providerRadio.checked = true;
  403. } else {
  404. document.getElementById('ledProviderNone').checked = true;
  405. }
  406. if (ledConfigData.wled_ip) {
  407. const wledIpInput = document.getElementById('wledIpInput');
  408. if (wledIpInput) wledIpInput.value = ledConfigData.wled_ip;
  409. }
  410. // Load DW LED settings
  411. if (ledConfigData.dw_led_num_leds) {
  412. const numLedsInput = document.getElementById('dwLedNumLeds');
  413. if (numLedsInput) numLedsInput.value = ledConfigData.dw_led_num_leds;
  414. }
  415. if (ledConfigData.dw_led_gpio_pin) {
  416. const gpioPinInput = document.getElementById('dwLedGpioPin');
  417. if (gpioPinInput) gpioPinInput.value = ledConfigData.dw_led_gpio_pin;
  418. }
  419. if (ledConfigData.dw_led_pixel_order) {
  420. const pixelOrderInput = document.getElementById('dwLedPixelOrder');
  421. if (pixelOrderInput) pixelOrderInput.value = ledConfigData.dw_led_pixel_order;
  422. }
  423. updateLedProviderUI()
  424. // Update version display
  425. const currentVersionText = document.getElementById('currentVersionText');
  426. const latestVersionText = document.getElementById('latestVersionText');
  427. const updateButton = document.getElementById('updateSoftware');
  428. const updateIcon = document.getElementById('updateIcon');
  429. const updateText = document.getElementById('updateText');
  430. if (currentVersionText) {
  431. currentVersionText.textContent = updateData.current;
  432. }
  433. if (latestVersionText) {
  434. if (updateData.error) {
  435. latestVersionText.textContent = 'Error checking updates';
  436. latestVersionText.className = 'text-red-500 text-sm font-normal leading-normal';
  437. } else {
  438. latestVersionText.textContent = updateData.latest;
  439. latestVersionText.className = 'text-slate-500 text-sm font-normal leading-normal';
  440. }
  441. }
  442. // Update button state
  443. if (updateButton && updateIcon && updateText) {
  444. if (updateData.update_available) {
  445. updateButton.disabled = false;
  446. 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';
  447. updateIcon.textContent = 'download';
  448. updateText.textContent = 'Update';
  449. } else {
  450. updateButton.disabled = true;
  451. 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';
  452. updateIcon.textContent = 'check';
  453. updateText.textContent = 'Up to date';
  454. }
  455. }
  456. // Update port selection
  457. const portSelect = document.getElementById('portSelect');
  458. if (portSelect) {
  459. // Clear existing options except the first one
  460. while (portSelect.options.length > 1) {
  461. portSelect.remove(1);
  462. }
  463. // Add new options
  464. ports.forEach(port => {
  465. const option = document.createElement('option');
  466. option.value = port;
  467. option.textContent = port;
  468. portSelect.appendChild(option);
  469. });
  470. // If there's exactly one port available, select it
  471. if (ports.length === 1) {
  472. portSelect.value = ports[0];
  473. }
  474. }
  475. // Update preferred port selection
  476. const preferredPortSelect = document.getElementById('preferredPortSelect');
  477. const currentPreferredPort = document.getElementById('currentPreferredPort');
  478. const preferredPortDisplay = document.getElementById('preferredPortDisplay');
  479. if (preferredPortSelect) {
  480. // Clear existing options except the first one (no preference)
  481. while (preferredPortSelect.options.length > 1) {
  482. preferredPortSelect.remove(1);
  483. }
  484. // Add all available ports
  485. ports.forEach(port => {
  486. const option = document.createElement('option');
  487. option.value = port;
  488. option.textContent = port;
  489. preferredPortSelect.appendChild(option);
  490. });
  491. // Set the current preferred port value
  492. if (preferredPortData && preferredPortData.preferred_port) {
  493. // Check if the preferred port is in the available ports
  494. const isAvailable = ports.includes(preferredPortData.preferred_port);
  495. if (isAvailable) {
  496. preferredPortSelect.value = preferredPortData.preferred_port;
  497. } else {
  498. // Add the preferred port as an option (it might not be currently available)
  499. const option = document.createElement('option');
  500. option.value = preferredPortData.preferred_port;
  501. option.textContent = `${preferredPortData.preferred_port} (not currently available)`;
  502. preferredPortSelect.appendChild(option);
  503. preferredPortSelect.value = preferredPortData.preferred_port;
  504. }
  505. // Show the current preferred port indicator
  506. if (currentPreferredPort && preferredPortDisplay) {
  507. preferredPortDisplay.textContent = `Currently set to: ${preferredPortData.preferred_port}`;
  508. currentPreferredPort.classList.remove('hidden');
  509. }
  510. }
  511. }
  512. // Initialize autocomplete for clear patterns
  513. const clearFromInInput = document.getElementById('customClearFromInInput');
  514. const clearFromOutInput = document.getElementById('customClearFromOutInput');
  515. if (clearFromInInput && clearFromOutInput && patterns && Array.isArray(patterns)) {
  516. // Store patterns globally for autocomplete
  517. window.availablePatterns = patterns;
  518. // Set current values if they exist
  519. if (clearPatterns && clearPatterns.custom_clear_from_in) {
  520. clearFromInInput.value = clearPatterns.custom_clear_from_in;
  521. }
  522. if (clearPatterns && clearPatterns.custom_clear_from_out) {
  523. clearFromOutInput.value = clearPatterns.custom_clear_from_out;
  524. }
  525. // Initialize autocomplete for both inputs
  526. initializeAutocomplete('customClearFromInInput', 'clearFromInSuggestions', 'clearFromInClear', patterns);
  527. initializeAutocomplete('customClearFromOutInput', 'clearFromOutSuggestions', 'clearFromOutClear', patterns);
  528. console.log('Autocomplete initialized with', patterns.length, 'patterns');
  529. }
  530. // Set clear pattern speed
  531. const clearPatternSpeedInput = document.getElementById('clearPatternSpeedInput');
  532. const effectiveClearSpeed = document.getElementById('effectiveClearSpeed');
  533. if (clearPatternSpeedInput && clearSpeedData) {
  534. // Only set value if clear_pattern_speed is not null
  535. if (clearSpeedData.clear_pattern_speed !== null && clearSpeedData.clear_pattern_speed !== undefined) {
  536. clearPatternSpeedInput.value = clearSpeedData.clear_pattern_speed;
  537. if (effectiveClearSpeed) {
  538. effectiveClearSpeed.textContent = `Current: ${clearSpeedData.clear_pattern_speed} steps/min`;
  539. }
  540. } else {
  541. // Leave empty to show placeholder for default
  542. clearPatternSpeedInput.value = '';
  543. if (effectiveClearSpeed && clearSpeedData.effective_speed) {
  544. effectiveClearSpeed.textContent = `Using default pattern speed: ${clearSpeedData.effective_speed} steps/min`;
  545. }
  546. }
  547. }
  548. // Update app name
  549. const appNameInput = document.getElementById('appNameInput');
  550. if (appNameInput && appNameData.app_name) {
  551. appNameInput.value = appNameData.app_name;
  552. }
  553. // Store Still Sands data for later initialization
  554. window.initialStillSandsData = scheduledPauseData;
  555. }).catch(error => {
  556. logMessage(`Error initializing settings page: ${error.message}`, LOG_TYPE.ERROR);
  557. });
  558. // Set up event listeners
  559. setupEventListeners();
  560. });
  561. // Setup event listeners
  562. function setupEventListeners() {
  563. // Save App Name
  564. const saveAppNameButton = document.getElementById('saveAppName');
  565. const appNameInput = document.getElementById('appNameInput');
  566. if (saveAppNameButton && appNameInput) {
  567. saveAppNameButton.addEventListener('click', async () => {
  568. const appName = appNameInput.value.trim() || 'Dune Weaver';
  569. try {
  570. const response = await fetch('/api/settings', {
  571. method: 'PATCH',
  572. headers: { 'Content-Type': 'application/json' },
  573. body: JSON.stringify({ app: { name: appName } })
  574. });
  575. if (response.ok) {
  576. await response.json();
  577. showStatusMessage('Application name updated successfully. Refresh the page to see changes.', 'success');
  578. // Update the page title and header immediately
  579. document.title = `Settings - ${appName}`;
  580. const headerTitle = document.querySelector('h1.text-gray-800');
  581. if (headerTitle) {
  582. // Update just the text content, preserving the connection status dot
  583. const textNode = headerTitle.childNodes[0];
  584. if (textNode && textNode.nodeType === Node.TEXT_NODE) {
  585. textNode.textContent = data.app_name;
  586. }
  587. }
  588. } else {
  589. throw new Error('Failed to save application name');
  590. }
  591. } catch (error) {
  592. showStatusMessage(`Failed to save application name: ${error.message}`, 'error');
  593. }
  594. });
  595. // Handle Enter key in app name input
  596. appNameInput.addEventListener('keypress', (e) => {
  597. if (e.key === 'Enter') {
  598. saveAppNameButton.click();
  599. }
  600. });
  601. }
  602. // LED provider selection change handlers
  603. const ledProviderRadios = document.querySelectorAll('input[name="ledProvider"]');
  604. ledProviderRadios.forEach(radio => {
  605. radio.addEventListener('change', updateLedProviderUI);
  606. });
  607. // Save LED configuration
  608. const saveLedConfig = document.getElementById('saveLedConfig');
  609. if (saveLedConfig) {
  610. saveLedConfig.addEventListener('click', async () => {
  611. const provider = document.querySelector('input[name="ledProvider"]:checked')?.value || 'none';
  612. let requestBody = { provider };
  613. if (provider === 'wled') {
  614. const wledIp = document.getElementById('wledIpInput')?.value;
  615. if (!wledIp) {
  616. showStatusMessage('Please enter a WLED IP address', 'error');
  617. return;
  618. }
  619. requestBody.ip_address = wledIp;
  620. } else if (provider === 'dw_leds') {
  621. const numLeds = parseInt(document.getElementById('dwLedNumLeds')?.value) || 60;
  622. const gpioPin = parseInt(document.getElementById('dwLedGpioPin')?.value) || 12;
  623. const pixelOrder = document.getElementById('dwLedPixelOrder')?.value || 'GRB';
  624. requestBody.num_leds = numLeds;
  625. requestBody.gpio_pin = gpioPin;
  626. requestBody.pixel_order = pixelOrder;
  627. }
  628. try {
  629. const response = await fetch('/set_led_config', {
  630. method: 'POST',
  631. headers: { 'Content-Type': 'application/json' },
  632. body: JSON.stringify(requestBody)
  633. });
  634. if (response.ok) {
  635. const data = await response.json();
  636. if (provider === 'wled' && data.wled_ip) {
  637. localStorage.setItem('wled_ip', data.wled_ip);
  638. showStatusMessage('WLED configured successfully', 'success');
  639. } else if (provider === 'dw_leds') {
  640. // Check if there's a warning (hardware not available but settings saved)
  641. if (data.warning) {
  642. showStatusMessage(
  643. `Settings saved for testing. Hardware issue: ${data.warning}`,
  644. 'warning'
  645. );
  646. } else {
  647. showStatusMessage(
  648. `DW LEDs configured: ${data.dw_led_num_leds} LEDs on GPIO${data.dw_led_gpio_pin}`,
  649. 'success'
  650. );
  651. }
  652. } else if (provider === 'none') {
  653. localStorage.removeItem('wled_ip');
  654. showStatusMessage('LED controller disabled', 'success');
  655. }
  656. } else {
  657. // Extract error detail from response
  658. const errorData = await response.json().catch(() => ({}));
  659. const errorMessage = errorData.detail || 'Failed to save LED configuration';
  660. showStatusMessage(errorMessage, 'error');
  661. }
  662. } catch (error) {
  663. showStatusMessage(`Failed to save LED configuration: ${error.message}`, 'error');
  664. }
  665. });
  666. }
  667. // Update software
  668. const updateSoftware = document.getElementById('updateSoftware');
  669. if (updateSoftware) {
  670. updateSoftware.addEventListener('click', async () => {
  671. if (updateSoftware.disabled) {
  672. return;
  673. }
  674. try {
  675. const response = await fetch('/api/update', {
  676. method: 'POST'
  677. });
  678. const data = await response.json();
  679. if (data.success) {
  680. showStatusMessage('Software update started successfully', 'success');
  681. } else if (data.manual_update_url) {
  682. // Show modal with manual update instructions, but use wiki link
  683. const wikiData = {
  684. ...data,
  685. manual_update_url: 'https://github.com/tuanchris/dune-weaver/wiki/Updating-software'
  686. };
  687. showUpdateInstructionsModal(wikiData);
  688. } else {
  689. showStatusMessage(data.message || 'No updates available', 'info');
  690. }
  691. } catch (error) {
  692. logMessage(`Error updating software: ${error.message}`, LOG_TYPE.ERROR);
  693. showStatusMessage('Failed to check for updates', 'error');
  694. }
  695. });
  696. }
  697. // Connect button
  698. const connectButton = document.getElementById('connectButton');
  699. if (connectButton) {
  700. connectButton.addEventListener('click', async () => {
  701. const portSelect = document.getElementById('portSelect');
  702. if (!portSelect || !portSelect.value) {
  703. logMessage('Please select a port first', LOG_TYPE.WARNING);
  704. return;
  705. }
  706. try {
  707. const response = await fetch('/connect', {
  708. method: 'POST',
  709. headers: { 'Content-Type': 'application/json' },
  710. body: JSON.stringify({ port: portSelect.value })
  711. });
  712. if (response.ok) {
  713. logMessage('Connected successfully', LOG_TYPE.SUCCESS);
  714. await updateSerialStatus(true); // Force update after connecting
  715. } else {
  716. throw new Error('Failed to connect');
  717. }
  718. } catch (error) {
  719. logMessage(`Error connecting to device: ${error.message}`, LOG_TYPE.ERROR);
  720. }
  721. });
  722. }
  723. // Disconnect button
  724. const disconnectButton = document.getElementById('disconnectButton');
  725. if (disconnectButton) {
  726. disconnectButton.addEventListener('click', async () => {
  727. try {
  728. const response = await fetch('/disconnect', {
  729. method: 'POST'
  730. });
  731. if (response.ok) {
  732. logMessage('Device disconnected successfully', LOG_TYPE.SUCCESS);
  733. await updateSerialStatus(true); // Force update after disconnecting
  734. } else {
  735. throw new Error('Failed to disconnect device');
  736. }
  737. } catch (error) {
  738. logMessage(`Error disconnecting device: ${error.message}`, LOG_TYPE.ERROR);
  739. }
  740. });
  741. }
  742. // Save preferred port button
  743. const savePreferredPortButton = document.getElementById('savePreferredPort');
  744. if (savePreferredPortButton) {
  745. savePreferredPortButton.addEventListener('click', savePreferredPort);
  746. }
  747. // Save custom clear patterns button
  748. const saveClearPatterns = document.getElementById('saveClearPatterns');
  749. if (saveClearPatterns) {
  750. saveClearPatterns.addEventListener('click', async () => {
  751. const clearFromInInput = document.getElementById('customClearFromInInput');
  752. const clearFromOutInput = document.getElementById('customClearFromOutInput');
  753. if (!clearFromInInput || !clearFromOutInput) {
  754. return;
  755. }
  756. // Validate that the entered patterns exist (if not empty)
  757. const inValue = clearFromInInput.value.trim();
  758. const outValue = clearFromOutInput.value.trim();
  759. if (inValue && window.availablePatterns && !window.availablePatterns.includes(inValue)) {
  760. showStatusMessage(`Pattern not found: ${inValue}`, 'error');
  761. return;
  762. }
  763. if (outValue && window.availablePatterns && !window.availablePatterns.includes(outValue)) {
  764. showStatusMessage(`Pattern not found: ${outValue}`, 'error');
  765. return;
  766. }
  767. try {
  768. const response = await fetch('/api/settings', {
  769. method: 'PATCH',
  770. headers: { 'Content-Type': 'application/json' },
  771. body: JSON.stringify({
  772. patterns: {
  773. custom_clear_from_in: inValue || null,
  774. custom_clear_from_out: outValue || null
  775. }
  776. })
  777. });
  778. if (response.ok) {
  779. showStatusMessage('Clear patterns saved successfully', 'success');
  780. } else {
  781. const error = await response.json();
  782. throw new Error(error.detail || 'Failed to save clear patterns');
  783. }
  784. } catch (error) {
  785. showStatusMessage(`Failed to save clear patterns: ${error.message}`, 'error');
  786. }
  787. });
  788. }
  789. // Logo upload functionality
  790. const logoFileInput = document.getElementById('logoFileInput');
  791. const resetLogoBtn = document.getElementById('resetLogoBtn');
  792. const logoPreview = document.getElementById('logoPreview');
  793. const logoUploadStatus = document.getElementById('logoUploadStatus');
  794. if (logoFileInput) {
  795. logoFileInput.addEventListener('change', async (event) => {
  796. const file = event.target.files[0];
  797. if (!file) return;
  798. // Show uploading status
  799. if (logoUploadStatus) {
  800. logoUploadStatus.textContent = 'Uploading...';
  801. logoUploadStatus.className = 'text-xs text-slate-500';
  802. }
  803. const formData = new FormData();
  804. formData.append('file', file);
  805. try {
  806. const response = await fetch('/api/upload-logo', {
  807. method: 'POST',
  808. body: formData
  809. });
  810. if (response.ok) {
  811. const data = await response.json();
  812. // Update preview image with cache-busting query param
  813. if (logoPreview) {
  814. logoPreview.src = data.url + '?t=' + Date.now();
  815. }
  816. // Show reset button
  817. if (resetLogoBtn) {
  818. resetLogoBtn.classList.remove('hidden');
  819. }
  820. // Show success status
  821. if (logoUploadStatus) {
  822. logoUploadStatus.textContent = 'Logo uploaded successfully!';
  823. logoUploadStatus.className = 'text-xs text-green-600';
  824. setTimeout(() => {
  825. logoUploadStatus.textContent = '';
  826. }, 3000);
  827. }
  828. showStatusMessage('Logo uploaded successfully. Refresh the page to see all changes.', 'success');
  829. } else {
  830. const error = await response.json();
  831. throw new Error(error.detail || 'Failed to upload logo');
  832. }
  833. } catch (error) {
  834. if (logoUploadStatus) {
  835. logoUploadStatus.textContent = `Error: ${error.message}`;
  836. logoUploadStatus.className = 'text-xs text-red-600';
  837. }
  838. showStatusMessage(`Failed to upload logo: ${error.message}`, 'error');
  839. }
  840. // Reset file input so the same file can be re-selected
  841. logoFileInput.value = '';
  842. });
  843. }
  844. if (resetLogoBtn) {
  845. resetLogoBtn.addEventListener('click', async () => {
  846. try {
  847. const response = await fetch('/api/custom-logo', {
  848. method: 'DELETE'
  849. });
  850. if (response.ok) {
  851. // Reset preview to default logo
  852. if (logoPreview) {
  853. logoPreview.src = '/static/apple-touch-icon.png?t=' + Date.now();
  854. }
  855. // Hide reset button
  856. resetLogoBtn.classList.add('hidden');
  857. showStatusMessage('Logo reset to default. Refresh the page to see all changes.', 'success');
  858. } else {
  859. const error = await response.json();
  860. throw new Error(error.detail || 'Failed to reset logo');
  861. }
  862. } catch (error) {
  863. showStatusMessage(`Failed to reset logo: ${error.message}`, 'error');
  864. }
  865. });
  866. }
  867. // Save clear pattern speed button
  868. const saveClearSpeed = document.getElementById('saveClearSpeed');
  869. if (saveClearSpeed) {
  870. saveClearSpeed.addEventListener('click', async () => {
  871. const clearPatternSpeedInput = document.getElementById('clearPatternSpeedInput');
  872. if (!clearPatternSpeedInput) {
  873. return;
  874. }
  875. let speed;
  876. if (clearPatternSpeedInput.value === '' || clearPatternSpeedInput.value === null) {
  877. // Empty value means use default (None)
  878. speed = null;
  879. } else {
  880. speed = parseInt(clearPatternSpeedInput.value);
  881. // Validate speed only if it's not null
  882. if (isNaN(speed) || speed < 50 || speed > 2000) {
  883. showStatusMessage('Clear pattern speed must be between 50 and 2000, or leave empty for default', 'error');
  884. return;
  885. }
  886. }
  887. try {
  888. const response = await fetch('/api/settings', {
  889. method: 'PATCH',
  890. headers: { 'Content-Type': 'application/json' },
  891. body: JSON.stringify({ patterns: { clear_pattern_speed: speed } })
  892. });
  893. if (response.ok) {
  894. await response.json();
  895. if (speed === null) {
  896. showStatusMessage('Clear pattern speed set to default', 'success');
  897. } else {
  898. showStatusMessage(`Clear pattern speed set to ${speed} steps/min`, 'success');
  899. }
  900. // Update the effective speed display
  901. const effectiveClearSpeed = document.getElementById('effectiveClearSpeed');
  902. if (effectiveClearSpeed) {
  903. if (speed === null) {
  904. effectiveClearSpeed.textContent = `Using default pattern speed: ${data.effective_speed} steps/min`;
  905. } else {
  906. effectiveClearSpeed.textContent = `Current: ${speed} steps/min`;
  907. }
  908. }
  909. } else {
  910. const error = await response.json();
  911. throw new Error(error.detail || 'Failed to save clear pattern speed');
  912. }
  913. } catch (error) {
  914. showStatusMessage(`Failed to save clear pattern speed: ${error.message}`, 'error');
  915. }
  916. });
  917. }
  918. }
  919. // Button click handlers
  920. document.addEventListener('DOMContentLoaded', function() {
  921. // Home button
  922. const homeButton = document.getElementById('homeButton');
  923. if (homeButton) {
  924. homeButton.addEventListener('click', async () => {
  925. try {
  926. const response = await fetch('/send_home', {
  927. method: 'POST',
  928. headers: {
  929. 'Content-Type': 'application/json'
  930. }
  931. });
  932. const data = await response.json();
  933. if (data.success) {
  934. updateStatus('Moving to home position...');
  935. }
  936. } catch (error) {
  937. console.error('Error sending home command:', error);
  938. updateStatus('Error: Failed to move to home position');
  939. }
  940. });
  941. }
  942. // Stop button
  943. const stopButton = document.getElementById('stopButton');
  944. if (stopButton) {
  945. stopButton.addEventListener('click', async () => {
  946. try {
  947. const response = await fetch('/stop_execution', {
  948. method: 'POST',
  949. headers: {
  950. 'Content-Type': 'application/json'
  951. }
  952. });
  953. const data = await response.json();
  954. if (data.success) {
  955. updateStatus('Execution stopped');
  956. }
  957. } catch (error) {
  958. console.error('Error stopping execution:', error);
  959. updateStatus('Error: Failed to stop execution');
  960. }
  961. });
  962. }
  963. // Move to Center button
  964. const centerButton = document.getElementById('centerButton');
  965. if (centerButton) {
  966. centerButton.addEventListener('click', async () => {
  967. try {
  968. const response = await fetch('/move_to_center', {
  969. method: 'POST',
  970. headers: {
  971. 'Content-Type': 'application/json'
  972. }
  973. });
  974. const data = await response.json();
  975. if (data.success) {
  976. updateStatus('Moving to center position...');
  977. }
  978. } catch (error) {
  979. console.error('Error moving to center:', error);
  980. updateStatus('Error: Failed to move to center');
  981. }
  982. });
  983. }
  984. // Move to Perimeter button
  985. const perimeterButton = document.getElementById('perimeterButton');
  986. if (perimeterButton) {
  987. perimeterButton.addEventListener('click', async () => {
  988. try {
  989. const response = await fetch('/move_to_perimeter', {
  990. method: 'POST',
  991. headers: {
  992. 'Content-Type': 'application/json'
  993. }
  994. });
  995. const data = await response.json();
  996. if (data.success) {
  997. updateStatus('Moving to perimeter position...');
  998. }
  999. } catch (error) {
  1000. console.error('Error moving to perimeter:', error);
  1001. updateStatus('Error: Failed to move to perimeter');
  1002. }
  1003. });
  1004. }
  1005. });
  1006. // Function to update status
  1007. function updateStatus(message) {
  1008. const statusElement = document.querySelector('.text-slate-800.text-base.font-medium.leading-normal');
  1009. if (statusElement) {
  1010. statusElement.textContent = message;
  1011. // Reset status after 3 seconds if it's a temporary message
  1012. if (message.includes('Moving') || message.includes('Execution')) {
  1013. setTimeout(() => {
  1014. statusElement.textContent = 'Status';
  1015. }, 3000);
  1016. }
  1017. }
  1018. }
  1019. // Function to show status messages (using existing base.js showStatusMessage if available)
  1020. function showStatusMessage(message, type) {
  1021. if (typeof window.showStatusMessage === 'function') {
  1022. window.showStatusMessage(message, type);
  1023. } else {
  1024. // Fallback to console logging
  1025. console.log(`[${type}] ${message}`);
  1026. }
  1027. }
  1028. // Function to show update instructions modal
  1029. function showUpdateInstructionsModal(data) {
  1030. // Create modal HTML
  1031. const modal = document.createElement('div');
  1032. modal.id = 'updateInstructionsModal';
  1033. modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4';
  1034. modal.innerHTML = `
  1035. <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md">
  1036. <div class="p-6">
  1037. <div class="text-center mb-4">
  1038. <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-2">Manual Update Required</h2>
  1039. <p class="text-gray-600 dark:text-gray-400 text-sm">
  1040. ${data.message}
  1041. </p>
  1042. </div>
  1043. <div class="text-gray-700 dark:text-gray-300 text-sm mb-6">
  1044. <p class="mb-3">${data.instructions}</p>
  1045. </div>
  1046. <div class="flex gap-3 justify-center">
  1047. <button id="openGitHubRelease" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
  1048. View Update Instructions
  1049. </button>
  1050. <button id="closeUpdateModal" class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors">
  1051. Close
  1052. </button>
  1053. </div>
  1054. </div>
  1055. </div>
  1056. `;
  1057. document.body.appendChild(modal);
  1058. // Add event listeners
  1059. const openGitHubButton = modal.querySelector('#openGitHubRelease');
  1060. const closeButton = modal.querySelector('#closeUpdateModal');
  1061. openGitHubButton.addEventListener('click', () => {
  1062. window.open(data.manual_update_url, '_blank');
  1063. document.body.removeChild(modal);
  1064. });
  1065. closeButton.addEventListener('click', () => {
  1066. document.body.removeChild(modal);
  1067. });
  1068. // Close on outside click
  1069. modal.addEventListener('click', (e) => {
  1070. if (e.target === modal) {
  1071. document.body.removeChild(modal);
  1072. }
  1073. });
  1074. }
  1075. // Autocomplete functionality
  1076. function initializeAutocomplete(inputId, suggestionsId, clearButtonId, patterns) {
  1077. const input = document.getElementById(inputId);
  1078. const suggestionsDiv = document.getElementById(suggestionsId);
  1079. const clearButton = document.getElementById(clearButtonId);
  1080. let selectedIndex = -1;
  1081. if (!input || !suggestionsDiv) return;
  1082. // Function to update clear button visibility
  1083. function updateClearButton() {
  1084. if (clearButton) {
  1085. if (input.value.trim()) {
  1086. clearButton.classList.remove('hidden');
  1087. } else {
  1088. clearButton.classList.add('hidden');
  1089. }
  1090. }
  1091. }
  1092. // Format pattern name for display
  1093. function formatPatternName(pattern) {
  1094. return pattern.replace('.thr', '').replace(/_/g, ' ');
  1095. }
  1096. // Filter patterns based on input
  1097. function filterPatterns(searchTerm) {
  1098. if (!searchTerm) return patterns.slice(0, 20); // Show first 20 when empty
  1099. const term = searchTerm.toLowerCase();
  1100. return patterns.filter(pattern => {
  1101. const name = pattern.toLowerCase();
  1102. return name.includes(term);
  1103. }).sort((a, b) => {
  1104. // Prioritize patterns that start with the search term
  1105. const aStarts = a.toLowerCase().startsWith(term);
  1106. const bStarts = b.toLowerCase().startsWith(term);
  1107. if (aStarts && !bStarts) return -1;
  1108. if (!aStarts && bStarts) return 1;
  1109. return a.localeCompare(b);
  1110. }).slice(0, 20); // Limit to 20 results
  1111. }
  1112. // Highlight matching text
  1113. function highlightMatch(text, searchTerm) {
  1114. if (!searchTerm) return text;
  1115. const regex = new RegExp(`(${searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
  1116. return text.replace(regex, '<mark>$1</mark>');
  1117. }
  1118. // Show suggestions
  1119. function showSuggestions(searchTerm) {
  1120. const filtered = filterPatterns(searchTerm);
  1121. if (filtered.length === 0 && searchTerm) {
  1122. suggestionsDiv.innerHTML = '<div class="suggestion-item" style="cursor: default; color: #9ca3af;">No patterns found</div>';
  1123. suggestionsDiv.classList.remove('hidden');
  1124. return;
  1125. }
  1126. suggestionsDiv.innerHTML = filtered.map((pattern, index) => {
  1127. const displayName = formatPatternName(pattern);
  1128. const highlighted = highlightMatch(displayName, searchTerm);
  1129. return `<div class="suggestion-item" data-value="${pattern}" data-index="${index}">${highlighted}</div>`;
  1130. }).join('');
  1131. suggestionsDiv.classList.remove('hidden');
  1132. selectedIndex = -1;
  1133. }
  1134. // Hide suggestions
  1135. function hideSuggestions() {
  1136. setTimeout(() => {
  1137. suggestionsDiv.classList.add('hidden');
  1138. selectedIndex = -1;
  1139. }, 200);
  1140. }
  1141. // Select suggestion
  1142. function selectSuggestion(value) {
  1143. input.value = value;
  1144. hideSuggestions();
  1145. updateClearButton();
  1146. }
  1147. // Handle keyboard navigation
  1148. function handleKeyboard(e) {
  1149. const items = suggestionsDiv.querySelectorAll('.suggestion-item[data-value]');
  1150. if (e.key === 'ArrowDown') {
  1151. e.preventDefault();
  1152. selectedIndex = Math.min(selectedIndex + 1, items.length - 1);
  1153. updateSelection(items);
  1154. } else if (e.key === 'ArrowUp') {
  1155. e.preventDefault();
  1156. selectedIndex = Math.max(selectedIndex - 1, -1);
  1157. updateSelection(items);
  1158. } else if (e.key === 'Enter') {
  1159. e.preventDefault();
  1160. if (selectedIndex >= 0 && items[selectedIndex]) {
  1161. selectSuggestion(items[selectedIndex].dataset.value);
  1162. } else if (items.length === 1) {
  1163. selectSuggestion(items[0].dataset.value);
  1164. }
  1165. } else if (e.key === 'Escape') {
  1166. hideSuggestions();
  1167. }
  1168. }
  1169. // Update visual selection
  1170. function updateSelection(items) {
  1171. items.forEach((item, index) => {
  1172. if (index === selectedIndex) {
  1173. item.classList.add('selected');
  1174. item.scrollIntoView({ block: 'nearest' });
  1175. } else {
  1176. item.classList.remove('selected');
  1177. }
  1178. });
  1179. }
  1180. // Event listeners
  1181. input.addEventListener('input', (e) => {
  1182. const value = e.target.value.trim();
  1183. updateClearButton();
  1184. if (value.length > 0 || e.target === document.activeElement) {
  1185. showSuggestions(value);
  1186. } else {
  1187. hideSuggestions();
  1188. }
  1189. });
  1190. input.addEventListener('focus', () => {
  1191. const value = input.value.trim();
  1192. showSuggestions(value);
  1193. });
  1194. input.addEventListener('blur', hideSuggestions);
  1195. input.addEventListener('keydown', handleKeyboard);
  1196. // Click handler for suggestions
  1197. suggestionsDiv.addEventListener('click', (e) => {
  1198. const item = e.target.closest('.suggestion-item[data-value]');
  1199. if (item) {
  1200. selectSuggestion(item.dataset.value);
  1201. }
  1202. });
  1203. // Mouse hover handler
  1204. suggestionsDiv.addEventListener('mouseover', (e) => {
  1205. const item = e.target.closest('.suggestion-item[data-value]');
  1206. if (item) {
  1207. selectedIndex = parseInt(item.dataset.index);
  1208. const items = suggestionsDiv.querySelectorAll('.suggestion-item[data-value]');
  1209. updateSelection(items);
  1210. }
  1211. });
  1212. // Clear button handler
  1213. if (clearButton) {
  1214. clearButton.addEventListener('click', () => {
  1215. input.value = '';
  1216. updateClearButton();
  1217. hideSuggestions();
  1218. input.focus();
  1219. });
  1220. }
  1221. // Initialize clear button visibility
  1222. updateClearButton();
  1223. }
  1224. // auto_play Mode Functions
  1225. async function initializeauto_playMode() {
  1226. const auto_playToggle = document.getElementById('auto_playModeToggle');
  1227. const auto_playSettings = document.getElementById('auto_playSettings');
  1228. const auto_playPlaylistSelect = document.getElementById('auto_playPlaylistSelect');
  1229. const auto_playRunModeSelect = document.getElementById('auto_playRunModeSelect');
  1230. const auto_playPauseTimeInput = document.getElementById('auto_playPauseTimeInput');
  1231. const auto_playClearPatternSelect = document.getElementById('auto_playClearPatternSelect');
  1232. const auto_playShuffleToggle = document.getElementById('auto_playShuffleToggle');
  1233. // Load current auto_play settings
  1234. try {
  1235. const response = await fetch('/api/auto_play-mode');
  1236. const data = await response.json();
  1237. auto_playToggle.checked = data.enabled;
  1238. if (data.enabled) {
  1239. auto_playSettings.style.display = 'block';
  1240. }
  1241. // Set current values
  1242. auto_playRunModeSelect.value = data.run_mode || 'loop';
  1243. auto_playPauseTimeInput.value = data.pause_time || 5.0;
  1244. auto_playClearPatternSelect.value = data.clear_pattern || 'adaptive';
  1245. auto_playShuffleToggle.checked = data.shuffle || false;
  1246. // Load playlists for selection
  1247. const playlistsResponse = await fetch('/list_all_playlists');
  1248. const playlists = await playlistsResponse.json();
  1249. // Clear and populate playlist select
  1250. auto_playPlaylistSelect.innerHTML = '<option value="">Select a playlist...</option>';
  1251. playlists.forEach(playlist => {
  1252. const option = document.createElement('option');
  1253. option.value = playlist;
  1254. option.textContent = playlist;
  1255. if (playlist === data.playlist) {
  1256. option.selected = true;
  1257. }
  1258. auto_playPlaylistSelect.appendChild(option);
  1259. });
  1260. } catch (error) {
  1261. logMessage(`Error loading auto_play settings: ${error.message}`, LOG_TYPE.ERROR);
  1262. }
  1263. // Get save button
  1264. const saveAutoPlayButton = document.getElementById('saveAutoPlaySettings');
  1265. // Function to save settings
  1266. async function saveSettings(showFeedback = false) {
  1267. const originalButtonHTML = saveAutoPlayButton ? saveAutoPlayButton.innerHTML : '';
  1268. if (showFeedback && saveAutoPlayButton) {
  1269. saveAutoPlayButton.disabled = true;
  1270. saveAutoPlayButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Saving...</span>';
  1271. }
  1272. try {
  1273. const response = await fetch('/api/auto_play-mode', {
  1274. method: 'POST',
  1275. headers: { 'Content-Type': 'application/json' },
  1276. body: JSON.stringify({
  1277. enabled: auto_playToggle.checked,
  1278. playlist: auto_playPlaylistSelect.value || null,
  1279. run_mode: auto_playRunModeSelect.value,
  1280. pause_time: parseFloat(auto_playPauseTimeInput.value) || 0,
  1281. clear_pattern: auto_playClearPatternSelect.value,
  1282. shuffle: auto_playShuffleToggle.checked
  1283. })
  1284. });
  1285. if (!response.ok) {
  1286. throw new Error('Failed to save settings');
  1287. }
  1288. if (showFeedback && saveAutoPlayButton) {
  1289. saveAutoPlayButton.innerHTML = '<span class="material-icons text-lg">check</span><span class="truncate">Saved!</span>';
  1290. showStatusMessage('Auto-play settings saved successfully', 'success');
  1291. setTimeout(() => {
  1292. saveAutoPlayButton.innerHTML = originalButtonHTML;
  1293. saveAutoPlayButton.disabled = false;
  1294. }, 2000);
  1295. }
  1296. } catch (error) {
  1297. logMessage(`Error saving auto_play settings: ${error.message}`, LOG_TYPE.ERROR);
  1298. if (showFeedback && saveAutoPlayButton) {
  1299. showStatusMessage(`Failed to save settings: ${error.message}`, 'error');
  1300. saveAutoPlayButton.innerHTML = originalButtonHTML;
  1301. saveAutoPlayButton.disabled = false;
  1302. }
  1303. }
  1304. }
  1305. // Toggle auto_play settings visibility and save
  1306. auto_playToggle.addEventListener('change', async () => {
  1307. auto_playSettings.style.display = auto_playToggle.checked ? 'block' : 'none';
  1308. await saveSettings(false); // Auto-save toggle state without full feedback
  1309. const statusText = auto_playToggle.checked ? 'enabled' : 'disabled';
  1310. showStatusMessage(`Auto-play ${statusText}`, 'success');
  1311. });
  1312. // Save button click handler
  1313. if (saveAutoPlayButton) {
  1314. saveAutoPlayButton.addEventListener('click', () => saveSettings(true));
  1315. }
  1316. }
  1317. // Initialize auto_play mode when DOM is ready
  1318. document.addEventListener('DOMContentLoaded', function() {
  1319. initializeauto_playMode();
  1320. initializeStillSandsMode();
  1321. initializeHomingConfig();
  1322. });
  1323. // Still Sands Mode Functions
  1324. async function initializeStillSandsMode() {
  1325. logMessage('Initializing Still Sands mode', LOG_TYPE.INFO);
  1326. const stillSandsToggle = document.getElementById('scheduledPauseToggle');
  1327. const stillSandsSettings = document.getElementById('scheduledPauseSettings');
  1328. const addTimeSlotButton = document.getElementById('addTimeSlotButton');
  1329. const saveStillSandsButton = document.getElementById('savePauseSettings');
  1330. const timeSlotsContainer = document.getElementById('timeSlotsContainer');
  1331. const wledControlToggle = document.getElementById('stillSandsWledControl');
  1332. const finishPatternToggle = document.getElementById('stillSandsFinishPattern');
  1333. const timezoneSelect = document.getElementById('stillSandsTimezone');
  1334. // Check if elements exist
  1335. if (!stillSandsToggle || !stillSandsSettings || !addTimeSlotButton || !saveStillSandsButton || !timeSlotsContainer) {
  1336. logMessage('Still Sands elements not found, skipping initialization', LOG_TYPE.WARNING);
  1337. logMessage(`Found elements: toggle=${!!stillSandsToggle}, settings=${!!stillSandsSettings}, addBtn=${!!addTimeSlotButton}, saveBtn=${!!saveStillSandsButton}, container=${!!timeSlotsContainer}`, LOG_TYPE.WARNING);
  1338. return;
  1339. }
  1340. logMessage('All Still Sands elements found successfully', LOG_TYPE.INFO);
  1341. // Track time slots
  1342. let timeSlots = [];
  1343. let slotIdCounter = 0;
  1344. // Load current Still Sands settings from initial data
  1345. try {
  1346. // Use the data loaded during page initialization, fallback to API if not available
  1347. let data;
  1348. if (window.initialStillSandsData) {
  1349. data = window.initialStillSandsData;
  1350. // Clear the global variable after use
  1351. delete window.initialStillSandsData;
  1352. } else {
  1353. // Fallback to API call if initial data not available
  1354. const response = await fetch('/api/scheduled-pause');
  1355. data = await response.json();
  1356. }
  1357. stillSandsToggle.checked = data.enabled || false;
  1358. if (data.enabled) {
  1359. stillSandsSettings.style.display = 'block';
  1360. }
  1361. // Load WLED control setting
  1362. if (wledControlToggle) {
  1363. wledControlToggle.checked = data.control_wled || false;
  1364. }
  1365. // Load finish pattern setting
  1366. if (finishPatternToggle) {
  1367. finishPatternToggle.checked = data.finish_pattern || false;
  1368. }
  1369. // Load timezone setting
  1370. if (timezoneSelect) {
  1371. timezoneSelect.value = data.timezone || '';
  1372. }
  1373. // Load existing time slots
  1374. timeSlots = data.time_slots || [];
  1375. // Assign IDs to loaded slots BEFORE rendering
  1376. if (timeSlots.length > 0) {
  1377. slotIdCounter = 0;
  1378. timeSlots.forEach(slot => {
  1379. slot.id = ++slotIdCounter;
  1380. });
  1381. }
  1382. renderTimeSlots();
  1383. } catch (error) {
  1384. logMessage(`Error loading Still Sands settings: ${error.message}`, LOG_TYPE.ERROR);
  1385. // Initialize with empty settings if load fails
  1386. timeSlots = [];
  1387. renderTimeSlots();
  1388. }
  1389. // Function to validate time format (HH:MM)
  1390. function isValidTime(timeString) {
  1391. const timeRegex = /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
  1392. return timeRegex.test(timeString);
  1393. }
  1394. // Function to create a new time slot element
  1395. function createTimeSlotElement(slot) {
  1396. const slotDiv = document.createElement('div');
  1397. slotDiv.className = 'time-slot-item';
  1398. slotDiv.dataset.slotId = slot.id;
  1399. slotDiv.innerHTML = `
  1400. <div class="flex items-center gap-3">
  1401. <div class="flex-1 grid grid-cols-1 md:grid-cols-2 gap-3">
  1402. <div class="flex flex-col gap-1">
  1403. <label class="text-slate-700 dark:text-slate-300 text-xs font-medium">Start Time</label>
  1404. <input
  1405. type="time"
  1406. 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"
  1407. value="${slot.start_time || ''}"
  1408. required
  1409. />
  1410. </div>
  1411. <div class="flex flex-col gap-1">
  1412. <label class="text-slate-700 dark:text-slate-300 text-xs font-medium">End Time</label>
  1413. <input
  1414. type="time"
  1415. 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"
  1416. value="${slot.end_time || ''}"
  1417. required
  1418. />
  1419. </div>
  1420. </div>
  1421. <div class="flex flex-col gap-1">
  1422. <label class="text-slate-700 dark:text-slate-300 text-xs font-medium">Days</label>
  1423. <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">
  1424. <option value="daily" ${slot.days === 'daily' ? 'selected' : ''}>Daily</option>
  1425. <option value="weekdays" ${slot.days === 'weekdays' ? 'selected' : ''}>Weekdays</option>
  1426. <option value="weekends" ${slot.days === 'weekends' ? 'selected' : ''}>Weekends</option>
  1427. <option value="custom" ${slot.days === 'custom' ? 'selected' : ''}>Custom</option>
  1428. </select>
  1429. </div>
  1430. <button
  1431. type="button"
  1432. 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"
  1433. title="Remove time slot"
  1434. >
  1435. <span class="material-icons text-base">delete</span>
  1436. </button>
  1437. </div>
  1438. <div class="custom-days-container mt-2" style="display: ${slot.days === 'custom' ? 'block' : 'none'};">
  1439. <label class="text-slate-700 dark:text-slate-300 text-xs font-medium mb-1 block">Select Days</label>
  1440. <div class="flex flex-wrap gap-2">
  1441. ${['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'].map(day => `
  1442. <label class="flex items-center gap-1 text-xs">
  1443. <input
  1444. type="checkbox"
  1445. name="custom-days-${slot.id}"
  1446. value="${day}"
  1447. ${slot.custom_days && slot.custom_days.includes(day) ? 'checked' : ''}
  1448. class="rounded border-slate-300 text-sky-600 focus:ring-sky-500"
  1449. />
  1450. <span class="text-slate-700 dark:text-slate-300 capitalize">${day.substring(0, 3)}</span>
  1451. </label>
  1452. `).join('')}
  1453. </div>
  1454. </div>
  1455. `;
  1456. // Add event listeners for this slot
  1457. const startTimeInput = slotDiv.querySelector('.start-time');
  1458. const endTimeInput = slotDiv.querySelector('.end-time');
  1459. const daysSelect = slotDiv.querySelector('.days-select');
  1460. const customDaysContainer = slotDiv.querySelector('.custom-days-container');
  1461. const removeButton = slotDiv.querySelector('.remove-slot-btn');
  1462. // Show/hide custom days based on selection
  1463. daysSelect.addEventListener('change', () => {
  1464. customDaysContainer.style.display = daysSelect.value === 'custom' ? 'block' : 'none';
  1465. updateTimeSlot(slot.id);
  1466. });
  1467. // Update slot data when inputs change
  1468. startTimeInput.addEventListener('change', () => updateTimeSlot(slot.id));
  1469. endTimeInput.addEventListener('change', () => updateTimeSlot(slot.id));
  1470. // Handle custom day checkboxes
  1471. customDaysContainer.addEventListener('change', () => updateTimeSlot(slot.id));
  1472. // Remove slot button
  1473. removeButton.addEventListener('click', () => {
  1474. removeTimeSlot(slot.id);
  1475. });
  1476. return slotDiv;
  1477. }
  1478. // Function to render all time slots
  1479. function renderTimeSlots() {
  1480. timeSlotsContainer.innerHTML = '';
  1481. if (timeSlots.length === 0) {
  1482. timeSlotsContainer.innerHTML = `
  1483. <div class="text-center py-8 text-slate-500 dark:text-slate-400">
  1484. <span class="material-icons text-4xl mb-2 block">schedule</span>
  1485. <p>No time slots configured</p>
  1486. <p class="text-xs mt-1">Click "Add Time Slot" to create a pause schedule</p>
  1487. </div>
  1488. `;
  1489. return;
  1490. }
  1491. timeSlots.forEach(slot => {
  1492. const slotElement = createTimeSlotElement(slot);
  1493. timeSlotsContainer.appendChild(slotElement);
  1494. });
  1495. }
  1496. // Function to add a new time slot
  1497. function addTimeSlot() {
  1498. const newSlot = {
  1499. id: ++slotIdCounter,
  1500. start_time: '22:00',
  1501. end_time: '08:00',
  1502. days: 'daily',
  1503. custom_days: []
  1504. };
  1505. timeSlots.push(newSlot);
  1506. renderTimeSlots();
  1507. }
  1508. // Function to remove a time slot
  1509. function removeTimeSlot(slotId) {
  1510. timeSlots = timeSlots.filter(slot => slot.id !== slotId);
  1511. renderTimeSlots();
  1512. }
  1513. // Function to update a time slot's data
  1514. function updateTimeSlot(slotId) {
  1515. const slotElement = timeSlotsContainer.querySelector(`[data-slot-id="${slotId}"]`);
  1516. if (!slotElement) return;
  1517. const slot = timeSlots.find(s => s.id === slotId);
  1518. if (!slot) return;
  1519. // Update slot data from inputs
  1520. slot.start_time = slotElement.querySelector('.start-time').value;
  1521. slot.end_time = slotElement.querySelector('.end-time').value;
  1522. slot.days = slotElement.querySelector('.days-select').value;
  1523. // Update custom days if applicable
  1524. if (slot.days === 'custom') {
  1525. const checkedDays = Array.from(slotElement.querySelectorAll(`input[name="custom-days-${slotId}"]:checked`))
  1526. .map(cb => cb.value);
  1527. slot.custom_days = checkedDays;
  1528. } else {
  1529. slot.custom_days = [];
  1530. }
  1531. }
  1532. // Function to validate all time slots
  1533. function validateTimeSlots() {
  1534. const errors = [];
  1535. timeSlots.forEach((slot, index) => {
  1536. if (!slot.start_time || !isValidTime(slot.start_time)) {
  1537. errors.push(`Time slot ${index + 1}: Invalid start time`);
  1538. }
  1539. if (!slot.end_time || !isValidTime(slot.end_time)) {
  1540. errors.push(`Time slot ${index + 1}: Invalid end time`);
  1541. }
  1542. if (slot.days === 'custom' && (!slot.custom_days || slot.custom_days.length === 0)) {
  1543. errors.push(`Time slot ${index + 1}: Please select at least one day for custom schedule`);
  1544. }
  1545. });
  1546. return errors;
  1547. }
  1548. // Function to save settings
  1549. async function saveStillSandsSettings() {
  1550. // Update all slots from current form values
  1551. timeSlots.forEach(slot => updateTimeSlot(slot.id));
  1552. // Validate time slots
  1553. const validationErrors = validateTimeSlots();
  1554. if (validationErrors.length > 0) {
  1555. showStatusMessage(`Validation errors: ${validationErrors.join(', ')}`, 'error');
  1556. return;
  1557. }
  1558. // Update button UI to show loading state
  1559. const originalButtonHTML = saveStillSandsButton.innerHTML;
  1560. saveStillSandsButton.disabled = true;
  1561. saveStillSandsButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Saving...</span>';
  1562. try {
  1563. const response = await fetch('/api/scheduled-pause', {
  1564. method: 'POST',
  1565. headers: { 'Content-Type': 'application/json' },
  1566. body: JSON.stringify({
  1567. enabled: stillSandsToggle.checked,
  1568. control_wled: wledControlToggle ? wledControlToggle.checked : false,
  1569. finish_pattern: finishPatternToggle ? finishPatternToggle.checked : false,
  1570. timezone: timezoneSelect ? (timezoneSelect.value || null) : null,
  1571. time_slots: timeSlots.map(slot => ({
  1572. start_time: slot.start_time,
  1573. end_time: slot.end_time,
  1574. days: slot.days,
  1575. custom_days: slot.custom_days
  1576. }))
  1577. })
  1578. });
  1579. if (!response.ok) {
  1580. const errorData = await response.json();
  1581. throw new Error(errorData.detail || 'Failed to save Still Sands settings');
  1582. }
  1583. // Show success state temporarily
  1584. saveStillSandsButton.innerHTML = '<span class="material-icons text-lg">check</span><span class="truncate">Saved!</span>';
  1585. showStatusMessage('Still Sands settings saved successfully', 'success');
  1586. // Restore button after 2 seconds
  1587. setTimeout(() => {
  1588. saveStillSandsButton.innerHTML = originalButtonHTML;
  1589. saveStillSandsButton.disabled = false;
  1590. }, 2000);
  1591. } catch (error) {
  1592. logMessage(`Error saving Still Sands settings: ${error.message}`, LOG_TYPE.ERROR);
  1593. showStatusMessage(`Failed to save settings: ${error.message}`, 'error');
  1594. // Restore button immediately on error
  1595. saveStillSandsButton.innerHTML = originalButtonHTML;
  1596. saveStillSandsButton.disabled = false;
  1597. }
  1598. }
  1599. // Note: Slot IDs are now assigned during initialization above, before first render
  1600. // Event listeners
  1601. stillSandsToggle.addEventListener('change', async () => {
  1602. logMessage(`Still Sands toggle changed: ${stillSandsToggle.checked}`, LOG_TYPE.INFO);
  1603. stillSandsSettings.style.display = stillSandsToggle.checked ? 'block' : 'none';
  1604. logMessage(`Settings display set to: ${stillSandsSettings.style.display}`, LOG_TYPE.INFO);
  1605. // Auto-save when toggle changes
  1606. try {
  1607. await saveStillSandsSettings();
  1608. const statusText = stillSandsToggle.checked ? 'enabled' : 'disabled';
  1609. showStatusMessage(`Still Sands ${statusText} successfully`, 'success');
  1610. } catch (error) {
  1611. logMessage(`Error saving Still Sands toggle: ${error.message}`, LOG_TYPE.ERROR);
  1612. showStatusMessage(`Failed to save Still Sands setting: ${error.message}`, 'error');
  1613. }
  1614. });
  1615. addTimeSlotButton.addEventListener('click', addTimeSlot);
  1616. saveStillSandsButton.addEventListener('click', saveStillSandsSettings);
  1617. // Add listener for WLED control toggle
  1618. if (wledControlToggle) {
  1619. wledControlToggle.addEventListener('change', async () => {
  1620. logMessage(`WLED control toggle changed: ${wledControlToggle.checked}`, LOG_TYPE.INFO);
  1621. // Auto-save when WLED control changes
  1622. await saveStillSandsSettings();
  1623. });
  1624. }
  1625. // Add listener for finish pattern toggle
  1626. if (finishPatternToggle) {
  1627. finishPatternToggle.addEventListener('change', async () => {
  1628. logMessage(`Finish pattern toggle changed: ${finishPatternToggle.checked}`, LOG_TYPE.INFO);
  1629. // Auto-save when finish pattern setting changes
  1630. await saveStillSandsSettings();
  1631. });
  1632. }
  1633. // Add listener for timezone select
  1634. if (timezoneSelect) {
  1635. timezoneSelect.addEventListener('change', async () => {
  1636. logMessage(`Timezone changed: ${timezoneSelect.value || 'System Default'}`, LOG_TYPE.INFO);
  1637. // Auto-save when timezone changes
  1638. await saveStillSandsSettings();
  1639. });
  1640. }
  1641. }
  1642. // Homing Configuration
  1643. async function initializeHomingConfig() {
  1644. logMessage('Initializing homing configuration', LOG_TYPE.INFO);
  1645. const homingModeCrash = document.getElementById('homingModeCrash');
  1646. const homingModeSensor = document.getElementById('homingModeSensor');
  1647. const angularOffsetInput = document.getElementById('angularOffsetInput');
  1648. const compassOffsetContainer = document.getElementById('compassOffsetContainer');
  1649. const saveHomingConfigButton = document.getElementById('saveHomingConfig');
  1650. const homingInfoContent = document.getElementById('homingInfoContent');
  1651. const autoHomeEnabledToggle = document.getElementById('autoHomeEnabledToggle');
  1652. const autoHomeSettings = document.getElementById('autoHomeSettings');
  1653. const autoHomeAfterPatternsInput = document.getElementById('autoHomeAfterPatternsInput');
  1654. // Check if elements exist
  1655. if (!homingModeCrash || !homingModeSensor || !angularOffsetInput || !saveHomingConfigButton || !homingInfoContent || !compassOffsetContainer) {
  1656. logMessage('Homing configuration elements not found, skipping initialization', LOG_TYPE.WARNING);
  1657. return;
  1658. }
  1659. logMessage('Homing configuration elements found successfully', LOG_TYPE.INFO);
  1660. // Function to get selected homing mode
  1661. function getSelectedMode() {
  1662. return homingModeCrash.checked ? 0 : 1;
  1663. }
  1664. // Function to update info box and visibility based on selected mode
  1665. function updateHomingInfo() {
  1666. const mode = getSelectedMode();
  1667. // Show/hide compass offset based on mode
  1668. if (mode === 0) {
  1669. compassOffsetContainer.style.display = 'none';
  1670. homingInfoContent.innerHTML = `
  1671. <p class="font-medium text-blue-800">Crash Homing Mode:</p>
  1672. <ul class="mt-1 space-y-1 text-blue-700">
  1673. <li>• Y axis moves -22mm (or -30mm for mini) until physical stop</li>
  1674. <li>• Theta set to 0, rho set to 0</li>
  1675. <li>• No x0 y0 command sent</li>
  1676. <li>• No hardware sensors required</li>
  1677. </ul>
  1678. `;
  1679. } else {
  1680. compassOffsetContainer.style.display = 'block';
  1681. homingInfoContent.innerHTML = `
  1682. <p class="font-medium text-blue-800">Sensor Homing Mode:</p>
  1683. <ul class="mt-1 space-y-1 text-blue-700">
  1684. <li>• Requires hardware limit switches</li>
  1685. <li>• Requires additional configuration</li>
  1686. </ul>
  1687. `;
  1688. }
  1689. }
  1690. // Load current homing configuration
  1691. try {
  1692. const response = await fetch('/api/homing-config');
  1693. const data = await response.json();
  1694. // Set radio button based on mode
  1695. if (data.homing_mode === 1) {
  1696. homingModeSensor.checked = true;
  1697. } else {
  1698. homingModeCrash.checked = true;
  1699. }
  1700. angularOffsetInput.value = data.angular_homing_offset_degrees || 0;
  1701. // Load auto-home settings
  1702. if (autoHomeEnabledToggle) {
  1703. autoHomeEnabledToggle.checked = data.auto_home_enabled || false;
  1704. if (autoHomeSettings) {
  1705. autoHomeSettings.style.display = data.auto_home_enabled ? 'block' : 'none';
  1706. }
  1707. }
  1708. if (autoHomeAfterPatternsInput) {
  1709. autoHomeAfterPatternsInput.value = data.auto_home_after_patterns || 5;
  1710. }
  1711. updateHomingInfo();
  1712. 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);
  1713. } catch (error) {
  1714. logMessage(`Error loading homing configuration: ${error.message}`, LOG_TYPE.ERROR);
  1715. // Initialize with defaults if load fails
  1716. homingModeCrash.checked = true;
  1717. angularOffsetInput.value = 0;
  1718. if (autoHomeEnabledToggle) autoHomeEnabledToggle.checked = false;
  1719. if (autoHomeAfterPatternsInput) autoHomeAfterPatternsInput.value = 5;
  1720. updateHomingInfo();
  1721. }
  1722. // Function to save homing configuration
  1723. async function saveHomingConfig() {
  1724. // Update button UI to show loading state
  1725. const originalButtonHTML = saveHomingConfigButton.innerHTML;
  1726. saveHomingConfigButton.disabled = true;
  1727. saveHomingConfigButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Saving...</span>';
  1728. try {
  1729. const requestBody = {
  1730. homing_mode: getSelectedMode(),
  1731. angular_homing_offset_degrees: parseFloat(angularOffsetInput.value) || 0
  1732. };
  1733. // Include auto-home settings if elements exist
  1734. if (autoHomeEnabledToggle) {
  1735. requestBody.auto_home_enabled = autoHomeEnabledToggle.checked;
  1736. }
  1737. if (autoHomeAfterPatternsInput) {
  1738. const afterPatterns = parseInt(autoHomeAfterPatternsInput.value);
  1739. if (!isNaN(afterPatterns) && afterPatterns >= 1) {
  1740. requestBody.auto_home_after_patterns = afterPatterns;
  1741. }
  1742. }
  1743. const response = await fetch('/api/homing-config', {
  1744. method: 'POST',
  1745. headers: { 'Content-Type': 'application/json' },
  1746. body: JSON.stringify(requestBody)
  1747. });
  1748. if (!response.ok) {
  1749. const errorData = await response.json();
  1750. throw new Error(errorData.detail || 'Failed to save homing configuration');
  1751. }
  1752. // Show success state temporarily
  1753. saveHomingConfigButton.innerHTML = '<span class="material-icons text-lg">check</span><span class="truncate">Saved!</span>';
  1754. showStatusMessage('Homing configuration saved successfully', 'success');
  1755. // Restore button after 2 seconds
  1756. setTimeout(() => {
  1757. saveHomingConfigButton.innerHTML = originalButtonHTML;
  1758. saveHomingConfigButton.disabled = false;
  1759. }, 2000);
  1760. } catch (error) {
  1761. logMessage(`Error saving homing configuration: ${error.message}`, LOG_TYPE.ERROR);
  1762. showStatusMessage(`Failed to save homing configuration: ${error.message}`, 'error');
  1763. // Restore button immediately on error
  1764. saveHomingConfigButton.innerHTML = originalButtonHTML;
  1765. saveHomingConfigButton.disabled = false;
  1766. }
  1767. }
  1768. // Event listeners
  1769. homingModeCrash.addEventListener('change', updateHomingInfo);
  1770. homingModeSensor.addEventListener('change', updateHomingInfo);
  1771. saveHomingConfigButton.addEventListener('click', saveHomingConfig);
  1772. // Auto-home toggle event listener
  1773. if (autoHomeEnabledToggle && autoHomeSettings) {
  1774. autoHomeEnabledToggle.addEventListener('change', () => {
  1775. autoHomeSettings.style.display = autoHomeEnabledToggle.checked ? 'block' : 'none';
  1776. });
  1777. }
  1778. }
  1779. // Toggle password visibility helper
  1780. function togglePasswordVisibility(inputId, button) {
  1781. const input = document.getElementById(inputId);
  1782. if (!input || !button) return;
  1783. const icon = button.querySelector('.material-icons');
  1784. if (input.type === 'password') {
  1785. input.type = 'text';
  1786. if (icon) icon.textContent = 'visibility';
  1787. } else {
  1788. input.type = 'password';
  1789. if (icon) icon.textContent = 'visibility_off';
  1790. }
  1791. }
  1792. // MQTT Configuration
  1793. async function initializeMqttConfig() {
  1794. logMessage('Initializing MQTT configuration', LOG_TYPE.INFO);
  1795. const mqttEnableToggle = document.getElementById('mqttEnableToggle');
  1796. const mqttSettings = document.getElementById('mqttSettings');
  1797. const mqttStatusBanner = document.getElementById('mqttStatusBanner');
  1798. const mqttConnectedBanner = document.getElementById('mqttConnectedBanner');
  1799. const mqttDisconnectedBanner = document.getElementById('mqttDisconnectedBanner');
  1800. const mqttBrokerInput = document.getElementById('mqttBrokerInput');
  1801. const mqttPortInput = document.getElementById('mqttPortInput');
  1802. const mqttUsernameInput = document.getElementById('mqttUsernameInput');
  1803. const mqttPasswordInput = document.getElementById('mqttPasswordInput');
  1804. const mqttDeviceNameInput = document.getElementById('mqttDeviceNameInput');
  1805. const mqttDeviceIdInput = document.getElementById('mqttDeviceIdInput');
  1806. const mqttClientIdInput = document.getElementById('mqttClientIdInput');
  1807. const mqttDiscoveryPrefixInput = document.getElementById('mqttDiscoveryPrefixInput');
  1808. const testMqttButton = document.getElementById('testMqttConnection');
  1809. const mqttTestResult = document.getElementById('mqttTestResult');
  1810. const saveMqttButton = document.getElementById('saveMqttConfig');
  1811. const mqttRestartNotice = document.getElementById('mqttRestartNotice');
  1812. // Check if elements exist
  1813. if (!mqttEnableToggle || !mqttSettings || !saveMqttButton) {
  1814. logMessage('MQTT configuration elements not found, skipping initialization', LOG_TYPE.WARNING);
  1815. return;
  1816. }
  1817. logMessage('MQTT configuration elements found successfully', LOG_TYPE.INFO);
  1818. // Track if settings have changed (to show restart notice)
  1819. let originalConfig = null;
  1820. let configChanged = false;
  1821. // Function to update UI based on enabled state
  1822. function updateMqttSettingsVisibility() {
  1823. mqttSettings.style.display = mqttEnableToggle.checked ? 'block' : 'none';
  1824. if (mqttStatusBanner) {
  1825. mqttStatusBanner.classList.toggle('hidden', !mqttEnableToggle.checked);
  1826. }
  1827. }
  1828. // Function to update connection status banners
  1829. function updateConnectionStatus(connected) {
  1830. if (mqttConnectedBanner && mqttDisconnectedBanner) {
  1831. if (connected) {
  1832. mqttConnectedBanner.classList.remove('hidden');
  1833. mqttDisconnectedBanner.classList.add('hidden');
  1834. } else {
  1835. mqttConnectedBanner.classList.add('hidden');
  1836. mqttDisconnectedBanner.classList.remove('hidden');
  1837. }
  1838. }
  1839. }
  1840. // Function to check if config has changed
  1841. function checkConfigChanged() {
  1842. if (!originalConfig) return false;
  1843. const currentConfig = {
  1844. enabled: mqttEnableToggle.checked,
  1845. broker: mqttBrokerInput.value,
  1846. port: parseInt(mqttPortInput.value) || 1883,
  1847. username: mqttUsernameInput.value,
  1848. password: mqttPasswordInput.value,
  1849. device_name: mqttDeviceNameInput.value,
  1850. device_id: mqttDeviceIdInput.value,
  1851. client_id: mqttClientIdInput.value,
  1852. discovery_prefix: mqttDiscoveryPrefixInput.value
  1853. };
  1854. return JSON.stringify(currentConfig) !== JSON.stringify(originalConfig);
  1855. }
  1856. // Function to show/hide restart notice
  1857. function updateRestartNotice() {
  1858. configChanged = checkConfigChanged();
  1859. if (mqttRestartNotice) {
  1860. mqttRestartNotice.classList.toggle('hidden', !configChanged);
  1861. }
  1862. }
  1863. // Load current MQTT configuration
  1864. try {
  1865. const response = await fetch('/api/mqtt-config');
  1866. const data = await response.json();
  1867. mqttEnableToggle.checked = data.enabled || false;
  1868. mqttBrokerInput.value = data.broker || '';
  1869. mqttPortInput.value = data.port || 1883;
  1870. mqttUsernameInput.value = data.username || '';
  1871. // Note: Password is not returned from API for security
  1872. mqttDeviceNameInput.value = data.device_name || 'Dune Weaver';
  1873. mqttDeviceIdInput.value = data.device_id || 'dune_weaver';
  1874. mqttClientIdInput.value = data.client_id || 'dune_weaver';
  1875. mqttDiscoveryPrefixInput.value = data.discovery_prefix || 'homeassistant';
  1876. // Store original config for change detection
  1877. originalConfig = {
  1878. enabled: data.enabled || false,
  1879. broker: data.broker || '',
  1880. port: data.port || 1883,
  1881. username: data.username || '',
  1882. password: '', // We don't have the original password
  1883. device_name: data.device_name || 'Dune Weaver',
  1884. device_id: data.device_id || 'dune_weaver',
  1885. client_id: data.client_id || 'dune_weaver',
  1886. discovery_prefix: data.discovery_prefix || 'homeassistant'
  1887. };
  1888. updateMqttSettingsVisibility();
  1889. // Update connection status if MQTT is enabled
  1890. if (data.enabled) {
  1891. updateConnectionStatus(data.connected || false);
  1892. }
  1893. logMessage(`Loaded MQTT config: enabled=${data.enabled}, broker=${data.broker}`, LOG_TYPE.INFO);
  1894. } catch (error) {
  1895. logMessage(`Error loading MQTT configuration: ${error.message}`, LOG_TYPE.ERROR);
  1896. // Initialize with defaults if load fails
  1897. mqttEnableToggle.checked = false;
  1898. updateMqttSettingsVisibility();
  1899. }
  1900. // Function to save MQTT configuration
  1901. async function saveMqttConfig() {
  1902. // Validate required fields if MQTT is enabled
  1903. if (mqttEnableToggle.checked && !mqttBrokerInput.value.trim()) {
  1904. showStatusMessage('MQTT broker address is required when MQTT is enabled', 'error');
  1905. mqttBrokerInput.focus();
  1906. return;
  1907. }
  1908. // Update button UI to show loading state
  1909. const originalButtonHTML = saveMqttButton.innerHTML;
  1910. saveMqttButton.disabled = true;
  1911. saveMqttButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Saving...</span>';
  1912. try {
  1913. const requestBody = {
  1914. enabled: mqttEnableToggle.checked,
  1915. broker: mqttBrokerInput.value.trim(),
  1916. port: parseInt(mqttPortInput.value) || 1883,
  1917. username: mqttUsernameInput.value.trim() || null,
  1918. device_name: mqttDeviceNameInput.value.trim() || 'Dune Weaver',
  1919. device_id: mqttDeviceIdInput.value.trim() || 'dune_weaver',
  1920. client_id: mqttClientIdInput.value.trim() || 'dune_weaver',
  1921. discovery_prefix: mqttDiscoveryPrefixInput.value.trim() || 'homeassistant'
  1922. };
  1923. // Only include password if it was changed (not empty)
  1924. if (mqttPasswordInput.value) {
  1925. requestBody.password = mqttPasswordInput.value;
  1926. }
  1927. const response = await fetch('/api/mqtt-config', {
  1928. method: 'POST',
  1929. headers: { 'Content-Type': 'application/json' },
  1930. body: JSON.stringify(requestBody)
  1931. });
  1932. if (!response.ok) {
  1933. const errorData = await response.json();
  1934. throw new Error(errorData.detail || 'Failed to save MQTT configuration');
  1935. }
  1936. const data = await response.json();
  1937. // Update original config for change detection
  1938. originalConfig = {
  1939. enabled: requestBody.enabled,
  1940. broker: requestBody.broker,
  1941. port: requestBody.port,
  1942. username: requestBody.username || '',
  1943. password: '', // Reset password tracking
  1944. device_name: requestBody.device_name,
  1945. device_id: requestBody.device_id,
  1946. client_id: requestBody.client_id,
  1947. discovery_prefix: requestBody.discovery_prefix
  1948. };
  1949. // Clear password field after save
  1950. mqttPasswordInput.value = '';
  1951. // Show success state temporarily
  1952. saveMqttButton.innerHTML = '<span class="material-icons text-lg">check</span><span class="truncate">Saved!</span>';
  1953. showStatusMessage('MQTT configuration saved successfully. Restart the application to apply changes.', 'success');
  1954. // Show restart notice
  1955. if (mqttRestartNotice) {
  1956. mqttRestartNotice.classList.remove('hidden');
  1957. }
  1958. // Restore button after 2 seconds
  1959. setTimeout(() => {
  1960. saveMqttButton.innerHTML = originalButtonHTML;
  1961. saveMqttButton.disabled = false;
  1962. }, 2000);
  1963. } catch (error) {
  1964. logMessage(`Error saving MQTT configuration: ${error.message}`, LOG_TYPE.ERROR);
  1965. showStatusMessage(`Failed to save MQTT configuration: ${error.message}`, 'error');
  1966. // Restore button immediately on error
  1967. saveMqttButton.innerHTML = originalButtonHTML;
  1968. saveMqttButton.disabled = false;
  1969. }
  1970. }
  1971. // Function to test MQTT connection
  1972. async function testMqttConnection() {
  1973. // Validate broker address
  1974. if (!mqttBrokerInput.value.trim()) {
  1975. showStatusMessage('Please enter a broker address to test', 'error');
  1976. mqttBrokerInput.focus();
  1977. return;
  1978. }
  1979. // Update button UI to show loading state
  1980. const originalButtonHTML = testMqttButton.innerHTML;
  1981. testMqttButton.disabled = true;
  1982. testMqttButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Testing...</span>';
  1983. // Clear previous result
  1984. if (mqttTestResult) {
  1985. mqttTestResult.innerHTML = '';
  1986. }
  1987. try {
  1988. const requestBody = {
  1989. broker: mqttBrokerInput.value.trim(),
  1990. port: parseInt(mqttPortInput.value) || 1883,
  1991. username: mqttUsernameInput.value.trim() || null,
  1992. password: mqttPasswordInput.value || null
  1993. };
  1994. const response = await fetch('/api/mqtt-test', {
  1995. method: 'POST',
  1996. headers: { 'Content-Type': 'application/json' },
  1997. body: JSON.stringify(requestBody)
  1998. });
  1999. const data = await response.json();
  2000. if (data.success) {
  2001. if (mqttTestResult) {
  2002. mqttTestResult.innerHTML = '<span class="material-icons text-green-600 mr-1">check_circle</span><span class="text-green-600">Connection successful!</span>';
  2003. }
  2004. showStatusMessage('MQTT connection test successful', 'success');
  2005. } else {
  2006. if (mqttTestResult) {
  2007. mqttTestResult.innerHTML = `<span class="material-icons text-red-600 mr-1">error</span><span class="text-red-600">${data.error || 'Connection failed'}</span>`;
  2008. }
  2009. showStatusMessage(`MQTT test failed: ${data.error || 'Connection failed'}`, 'error');
  2010. }
  2011. } catch (error) {
  2012. logMessage(`Error testing MQTT connection: ${error.message}`, LOG_TYPE.ERROR);
  2013. if (mqttTestResult) {
  2014. mqttTestResult.innerHTML = `<span class="material-icons text-red-600 mr-1">error</span><span class="text-red-600">Test failed: ${error.message}</span>`;
  2015. }
  2016. showStatusMessage(`MQTT test failed: ${error.message}`, 'error');
  2017. } finally {
  2018. // Restore button
  2019. testMqttButton.innerHTML = originalButtonHTML;
  2020. testMqttButton.disabled = false;
  2021. }
  2022. }
  2023. // Event listeners
  2024. mqttEnableToggle.addEventListener('change', () => {
  2025. updateMqttSettingsVisibility();
  2026. updateRestartNotice();
  2027. });
  2028. // Track changes to show restart notice
  2029. [mqttBrokerInput, mqttPortInput, mqttUsernameInput, mqttPasswordInput,
  2030. mqttDeviceNameInput, mqttDeviceIdInput, mqttClientIdInput, mqttDiscoveryPrefixInput].forEach(input => {
  2031. if (input) {
  2032. input.addEventListener('input', updateRestartNotice);
  2033. }
  2034. });
  2035. testMqttButton.addEventListener('click', testMqttConnection);
  2036. saveMqttButton.addEventListener('click', saveMqttConfig);
  2037. }
  2038. // Initialize MQTT config when DOM is ready
  2039. document.addEventListener('DOMContentLoaded', function() {
  2040. initializeMqttConfig();
  2041. initializeTableTypeConfig();
  2042. });
  2043. // ============================================================================
  2044. // Table Type Configuration
  2045. // ============================================================================
  2046. function initializeTableTypeConfig() {
  2047. const tableTypeSelect = document.getElementById('tableTypeSelect');
  2048. const saveTableTypeButton = document.getElementById('saveTableType');
  2049. const detectedTableType = document.getElementById('detectedTableType');
  2050. if (!tableTypeSelect || !saveTableTypeButton) {
  2051. logMessage('Table type elements not found', LOG_TYPE.WARNING);
  2052. return;
  2053. }
  2054. // Load current settings
  2055. loadTableTypeSettings();
  2056. // Save button click handler
  2057. saveTableTypeButton.addEventListener('click', saveTableTypeConfig);
  2058. async function loadTableTypeSettings() {
  2059. try {
  2060. const response = await fetch('/api/settings');
  2061. if (!response.ok) throw new Error('Failed to fetch settings');
  2062. const settings = await response.json();
  2063. const machine = settings.machine || {};
  2064. // Populate dropdown with available table types
  2065. tableTypeSelect.innerHTML = '<option value="">Auto-detect (use detected type)</option>';
  2066. if (machine.available_table_types) {
  2067. machine.available_table_types.forEach(type => {
  2068. const option = document.createElement('option');
  2069. option.value = type.value;
  2070. option.textContent = type.label;
  2071. tableTypeSelect.appendChild(option);
  2072. });
  2073. }
  2074. // Set current override value
  2075. if (machine.table_type_override) {
  2076. tableTypeSelect.value = machine.table_type_override;
  2077. } else {
  2078. tableTypeSelect.value = '';
  2079. }
  2080. // Update detected type display
  2081. if (detectedTableType) {
  2082. const detected = machine.detected_table_type;
  2083. if (detected) {
  2084. // Find the label for the detected type
  2085. const typeInfo = machine.available_table_types?.find(t => t.value === detected);
  2086. detectedTableType.textContent = typeInfo ? typeInfo.label : detected;
  2087. } else {
  2088. detectedTableType.textContent = 'Not connected';
  2089. }
  2090. }
  2091. logMessage('Table type settings loaded', LOG_TYPE.DEBUG);
  2092. } catch (error) {
  2093. logMessage(`Error loading table type settings: ${error.message}`, LOG_TYPE.ERROR);
  2094. }
  2095. }
  2096. async function saveTableTypeConfig() {
  2097. const originalButtonHTML = saveTableTypeButton.innerHTML;
  2098. saveTableTypeButton.disabled = true;
  2099. saveTableTypeButton.innerHTML = '<span class="material-icons text-lg animate-spin">refresh</span><span class="truncate">Saving...</span>';
  2100. try {
  2101. const response = await fetch('/api/settings', {
  2102. method: 'PATCH',
  2103. headers: { 'Content-Type': 'application/json' },
  2104. body: JSON.stringify({
  2105. machine: {
  2106. table_type_override: tableTypeSelect.value || ''
  2107. }
  2108. })
  2109. });
  2110. if (!response.ok) throw new Error('Failed to save settings');
  2111. const result = await response.json();
  2112. if (result.success) {
  2113. showStatusMessage('Table type settings saved. Changes will take effect on next connection.', 'success');
  2114. // Reload to show updated effective type
  2115. await loadTableTypeSettings();
  2116. } else {
  2117. throw new Error('Save failed');
  2118. }
  2119. } catch (error) {
  2120. logMessage(`Error saving table type: ${error.message}`, LOG_TYPE.ERROR);
  2121. showStatusMessage(`Failed to save table type: ${error.message}`, 'error');
  2122. } finally {
  2123. saveTableTypeButton.innerHTML = originalButtonHTML;
  2124. saveTableTypeButton.disabled = false;
  2125. }
  2126. }
  2127. }