init
This commit is contained in:
		
							
								
								
									
										70
									
								
								html/bambu_filaments.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								html/bambu_filaments.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| { | ||||
|     "GFU99": "Generic TPU", | ||||
|     "GFN99": "Generic PA", | ||||
|     "GFN98": "Generic PA-CF", | ||||
|     "GFA01": "Bambu PLA Matte", | ||||
|     "GFA00": "Bambu PLA Basic", | ||||
|     "GFA09": "Bambu PLA Tough", | ||||
|     "GFA07": "Bambu PLA Marble", | ||||
|     "GFA08": "Bambu PLA Sparkle", | ||||
|     "GFA02": "Bambu PLA Metal", | ||||
|     "GFA05": "Bambu PLA Silk", | ||||
|     "GFS00": "Bambu Support W", | ||||
|     "GFL03": "eSUN PLA+", | ||||
|     "GFL01": "PolyTerra PLA", | ||||
|     "GFL00": "PolyLite PLA", | ||||
|     "GFL99": "Generic PLA", | ||||
|     "GFL96": "Generic PLA Silk", | ||||
|     "GFL98": "Generic PLA-CF", | ||||
|     "GFA50": "Bambu PLA-CF", | ||||
|     "GFS02": "Bambu Support For PLA", | ||||
|     "GFA11": "Bambu PLA Aero", | ||||
|     "GFL04": "Overture PLA", | ||||
|     "GFL05": "Overture Matte PLA", | ||||
|     "GFL95": "Generic PLA High Speed", | ||||
|     "GFA12": "Bambu PLA Glow", | ||||
|     "GFA13": "Bambu PLA Dynamic", | ||||
|     "GFA15": "Bambu PLA Galaxy", | ||||
|     "GFS05": "Bambu Support For PLA/PETG", | ||||
|     "GFU01": "Bambu TPU 95A", | ||||
|     "GFU00": "Bambu TPU 95A HF", | ||||
|     "GFG00": "Bambu PETG Basic", | ||||
|     "GFT01": "Bambu PET-CF", | ||||
|     "GFG99": "Generic PETG", | ||||
|     "GFG98": "Generic PETG-CF", | ||||
|     "GFG50": "Bambu PETG-CF", | ||||
|     "GFG60": "PolyLite PETG", | ||||
|     "GFG01": "Bambu PETG Translucent", | ||||
|     "GFG97": "Generic PCTG", | ||||
|     "GFB00": "Bambu ABS", | ||||
|     "GFB99": "Generic ABS", | ||||
|     "GFB60": "PolyLite ABS", | ||||
|     "GFB50": "Bambu ABS-GF", | ||||
|     "GFC00": "Bambu PC", | ||||
|     "GFC99": "Generic PC", | ||||
|     "GFB98": "Generic ASA", | ||||
|     "GFB01": "Bambu ASA", | ||||
|     "GFB61": "PolyLite ASA", | ||||
|     "GFB02": "Bambu ASA-Aero", | ||||
|     "GFS99": "Generic PVA", | ||||
|     "GFS04": "Bambu PVA", | ||||
|     "GFS01": "Bambu Support G", | ||||
|     "GFN03": "Bambu PA-CF", | ||||
|     "GFN04": "Bambu PAHT-CF", | ||||
|     "GFS03": "Bambu Support For PA/PET", | ||||
|     "GFN05": "Bambu PA6-CF", | ||||
|     "GFN08": "Bambu PA6-GF", | ||||
|     "GFS98": "Generic HIPS", | ||||
|     "GFT98": "Generic PPS-CF", | ||||
|     "GFT97": "Generic PPS", | ||||
|     "GFN97": "Generic PPA-CF", | ||||
|     "GFN96": "Generic PPA-GF", | ||||
|     "GFP99": "Generic PE", | ||||
|     "GFP98": "Generic PE-CF", | ||||
|     "GFP97": "Generic PP", | ||||
|     "GFP96": "Generic PP-CF", | ||||
|     "GFP95": "Generic PP-GF", | ||||
|     "GFR99": "Generic EVA", | ||||
|     "GFR98": "Generic PHA", | ||||
|     "GFS97": "Generic BVOH" | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								html/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								html/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 17 KiB | 
							
								
								
									
										51
									
								
								html/header.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								html/header.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <title>FilaMan - Filament Management Tool</title> | ||||
|     <link rel="icon" type="image/png" href="/favicon.ico"> | ||||
|     <link rel="stylesheet" href="style.css"> | ||||
|     <style> | ||||
|         .status-container { | ||||
|             float: right; | ||||
|             display: flex; | ||||
|             gap: 10px; | ||||
|             align-items: center; | ||||
|             margin-right: 10px; | ||||
|         } | ||||
|         .status-dot { | ||||
|             width: 8px; | ||||
|             height: 8px; | ||||
|             border-radius: 50%; | ||||
|             display: inline-block; | ||||
|             margin-right: 4px; | ||||
|         } | ||||
|         .status-item { | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|             font-size: 0.8em; | ||||
|             color: #fff; | ||||
|         } | ||||
|         .online { background-color: #2ecc71; } | ||||
|         .offline { background-color: #e74c3c; } | ||||
|         .ram-status { color: #fff; font-size: 0.8em; } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <div class="navbar"> | ||||
|         <img src="/logo.png" alt="FilaMan Logo" class="logo"> | ||||
|         <a href="/">Start</a> | ||||
|         <a href="/waage">Scale</a> | ||||
|         <a href="/spoolman">Spoolman/Bambu</a> | ||||
|         <a href="/about">About</a> | ||||
|         <div class="status-container"> | ||||
|             <div class="status-item"> | ||||
|                 <span class="status-dot" id="bambuDot"></span>B | ||||
|             </div> | ||||
|             <div class="status-item"> | ||||
|                 <span class="status-dot" id="spoolmanDot"></span>S | ||||
|             </div> | ||||
|             <div class="ram-status" id="ramStatus"></div> | ||||
|         </div> | ||||
|     </div> | ||||
							
								
								
									
										37
									
								
								html/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								html/index.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| {{header}} | ||||
|  | ||||
|     <div class="container"> | ||||
|         <h1>FilaMan</h1> | ||||
|         <p>Filament Management Tool</p> | ||||
|         <p>Your smart solution for <strong>Filament Management</strong> in 3D printing.</p> | ||||
|  | ||||
|         <h2>About FilaMan</h2> | ||||
|         <p> | ||||
|             FilaMan is a tool designed to simplify filament spool management. It allows you to identify and weigh filament spools,  | ||||
|             automatically sync data with the self-hosted <a href="https://github.com/Donkie/Spoolman" target="_blank">Spoolman</a> platform,  | ||||
|             and ensure compatibility with <a href="https://github.com/spuder/OpenSpool" target="_blank">OpenSpool</a> for Bambu printers. | ||||
|         </p> | ||||
|  | ||||
|         <div class="features"> | ||||
|             <div class="feature"> | ||||
|                 <h3>Spool Identification</h3> | ||||
|                 <p>Easily identify filament spools using NFC tags (NTag215 or larger).</p> | ||||
|             </div> | ||||
|             <div class="feature"> | ||||
|                 <h3>Automatic Syncing</h3> | ||||
|                 <p>Seamlessly update spool data with Spoolman for accurate tracking.</p> | ||||
|             </div> | ||||
|             <div class="feature"> | ||||
|                 <h3>OpenSpool Compatibility</h3> | ||||
|                 <p>Works with OpenSpool to recognize and activate spools on Bambu printers.</p> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <h2>Future Plans</h2> | ||||
|         <p> | ||||
|             We are working on expanding compatibility to support smaller NFC tags like NTag213  | ||||
|             and developing custom software to enhance the OpenSpool experience. | ||||
|         </p> | ||||
|     </div> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										
											BIN
										
									
								
								html/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								html/logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 8.6 KiB | 
							
								
								
									
										114
									
								
								html/rfid.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								html/rfid.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| {{header}} | ||||
| <div class="connection-status hidden"> | ||||
|     <div class="spinner"></div> | ||||
|     <span>Connection lost. Trying to reconnect...</span> | ||||
| </div> | ||||
| <div class="content"> | ||||
|     <div class="three-column-layout"> | ||||
|         <!-- Linke Spalte --> | ||||
|         <div class="column"> | ||||
|             <div class="feature-box"> | ||||
|                 <div class="statistics-header"> | ||||
|                     <h2>Statistics</h2> | ||||
|                     <button id="refreshSpoolman" class="refresh-button"> | ||||
|                         <span>Refresh Spoolman</span> | ||||
|                     </button> | ||||
|                 </div> | ||||
|                 <div class="statistics-column"> | ||||
|                     <h3>Spools</h3> | ||||
|                         <div class="spool-stat" style="display: flex; justify-content: center; align-items: center;"> | ||||
|                             <span class="stat-label">total:</span> | ||||
|                             <span class="stat-value" id="totalSpools"></span> | ||||
|                             <div style="width: auto;"></div> | ||||
|                             <span class="stat-label">without Tag:</span> | ||||
|                             <span class="stat-value" id="spoolsWithoutTag"></span> | ||||
|                         </div> | ||||
|                 </div> | ||||
|  | ||||
|                 <div class="statistics-grid"> | ||||
|                     <div class="statistics-column"> | ||||
|                         <h3>Overview</h3> | ||||
|                         <ul class="statistics-list"> | ||||
|                             <li> | ||||
|                                 <span class="stat-label">Manufacturer:</span> | ||||
|                                 <span class="stat-value" id="totalVendors"></span> | ||||
|                             </li> | ||||
|                             <li> | ||||
|                                 <span class="stat-label">Weight:</span> | ||||
|                                 <span class="stat-value"><span id="totalWeight"></span> kg</span> | ||||
|                             </li> | ||||
|                             <li> | ||||
|                                 <span class="stat-label">Length:</span> | ||||
|                                 <span class="stat-value"><span id="totalLength"></span> m</span> | ||||
|                             </li> | ||||
|                         </ul> | ||||
|                     </div> | ||||
|                     <div class="statistics-column"> | ||||
|                         <h3>Materials</h3> | ||||
|                         <ul class="statistics-list" id="materialsList"> | ||||
|                             <!-- Wird dynamisch befüllt --> | ||||
|                         </ul> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="feature-box"> | ||||
|                 <div class="nfc-header"> | ||||
|                     <h2>NFC-Tag</h2> | ||||
|                     <span id="nfcStatusIndicator" class="status-circle"></span> | ||||
|                 </div> | ||||
|                 <div class="nfc-status-display"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <!-- Mittlere Spalte --> | ||||
|         <div class="column"> | ||||
|             <div class="feature-box"> | ||||
|                 <h2>Spoolman Spools</h2> | ||||
|                 <h2>1. select Manufacturer</h2> | ||||
|                 <label for="vendorSelect">Manufacturer:</label> | ||||
|                 <div style="display: flex; justify-content: space-between; align-items: center;"> | ||||
|                     <select id="vendorSelect" class="styled-select"> | ||||
|                         <option value="">Please choose...</option> | ||||
|                     </select> | ||||
|                     <label style="margin-left: 10px;"> | ||||
|                         <input type="checkbox" id="onlyWithoutSmId" checked onchange="updateFilamentDropdown()"> | ||||
|                         Only Spools without SM ID | ||||
|                     </label> | ||||
|                 </div> | ||||
|             </div> | ||||
|  | ||||
|             <div id="filamentSection" class="feature-box hidden"> | ||||
|                 <h2>2. Select Spool</h2> | ||||
|                 <label>Spool / Filament:</label> | ||||
|                 <div class="custom-dropdown"> | ||||
|                     <div class="dropdown-button" onclick="toggleFilamentDropdown()"> | ||||
|                         <div class="selected-color" id="selected-color"></div> | ||||
|                         <span id="selected-filament">Please choose...</span> | ||||
|                         <span class="dropdown-arrow">▼</span> | ||||
|                     </div> | ||||
|                     <div class="dropdown-content" id="filament-dropdown-content"> | ||||
|                         <!-- Optionen werden dynamisch hinzugefügt --> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <p id="nfcInfo" class="nfc-status"></p> | ||||
|                 <button id="writeNfcButton" class="btn btn-primary hidden" onclick="writeNfcTag()">Write Tag</button> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <!-- Rechte Spalte --> | ||||
|         <div class="column"> | ||||
|             <div class="feature-box"> | ||||
|                 <h2>Bambu AMS</h2> | ||||
|                 <div id="amsDataContainer"> | ||||
|                     <div class="amsData" id="amsData">Wait for AMS-Data...</div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| <script src="spoolman.js"></script> | ||||
| <script src="rfid.js"></script> | ||||
|  | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										570
									
								
								html/rfid.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										570
									
								
								html/rfid.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,570 @@ | ||||
| // WebSocket Variablen | ||||
| let socket; | ||||
| let isConnected = false; | ||||
| const RECONNECT_INTERVAL = 5000; | ||||
| const HEARTBEAT_INTERVAL = 10000; | ||||
| let heartbeatTimer = null; | ||||
| let lastHeartbeatResponse = Date.now(); | ||||
| const HEARTBEAT_TIMEOUT = 20000; | ||||
| let reconnectTimer = null; | ||||
|  | ||||
| // WebSocket Funktionen | ||||
| function startHeartbeat() { | ||||
|     if (heartbeatTimer) clearInterval(heartbeatTimer); | ||||
|      | ||||
|     heartbeatTimer = setInterval(() => { | ||||
|         // Prüfe ob zu lange keine Antwort kam | ||||
|         if (Date.now() - lastHeartbeatResponse > HEARTBEAT_TIMEOUT) { | ||||
|             isConnected = false; | ||||
|             updateConnectionStatus(); | ||||
|             if (socket) { | ||||
|                 socket.close(); | ||||
|                 socket = null; | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!socket || socket.readyState !== WebSocket.OPEN) { | ||||
|             isConnected = false; | ||||
|             updateConnectionStatus(); | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|         try { | ||||
|             // Sende Heartbeat | ||||
|             socket.send(JSON.stringify({ type: 'heartbeat' })); | ||||
|         } catch (error) { | ||||
|             isConnected = false; | ||||
|             updateConnectionStatus(); | ||||
|             if (socket) { | ||||
|                 socket.close(); | ||||
|                 socket = null; | ||||
|             } | ||||
|         } | ||||
|     }, HEARTBEAT_INTERVAL); | ||||
| } | ||||
|  | ||||
| function initWebSocket() { | ||||
|     // Clear any existing reconnect timer | ||||
|     if (reconnectTimer) { | ||||
|         clearTimeout(reconnectTimer); | ||||
|         reconnectTimer = null; | ||||
|     } | ||||
|  | ||||
|     // Wenn eine existierende Verbindung besteht, diese erst schließen | ||||
|     if (socket) { | ||||
|         socket.close(); | ||||
|         socket = null; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|         socket = new WebSocket('ws://' + window.location.host + '/ws'); | ||||
|          | ||||
|         socket.onopen = function() { | ||||
|             isConnected = true; | ||||
|             updateConnectionStatus(); | ||||
|             startHeartbeat(); // Starte Heartbeat nach erfolgreicher Verbindung | ||||
|         }; | ||||
|          | ||||
|         socket.onclose = function() { | ||||
|             isConnected = false; | ||||
|             updateConnectionStatus(); | ||||
|             if (heartbeatTimer) clearInterval(heartbeatTimer); | ||||
|              | ||||
|             // Nur neue Verbindung versuchen, wenn kein Timer läuft | ||||
|             if (!reconnectTimer) { | ||||
|                 reconnectTimer = setTimeout(() => { | ||||
|                     initWebSocket(); | ||||
|                 }, RECONNECT_INTERVAL); | ||||
|             } | ||||
|         }; | ||||
|          | ||||
|         socket.onerror = function(error) { | ||||
|             isConnected = false; | ||||
|             updateConnectionStatus(); | ||||
|             if (heartbeatTimer) clearInterval(heartbeatTimer); | ||||
|              | ||||
|             // Bei Fehler Verbindung schließen und neu aufbauen | ||||
|             if (socket) { | ||||
|                 socket.close(); | ||||
|                 socket = null; | ||||
|             } | ||||
|         }; | ||||
|          | ||||
|         socket.onmessage = function(event) { | ||||
|             lastHeartbeatResponse = Date.now(); // Aktualisiere Zeitstempel bei jeder Server-Antwort | ||||
|              | ||||
|             const data = JSON.parse(event.data); | ||||
|             if (data.type === 'amsData') { | ||||
|                 displayAmsData(data.payload); | ||||
|             } else if (data.type === 'nfcTag') { | ||||
|                 updateNfcStatusIndicator(data.payload); | ||||
|             } else if (data.type === 'nfcData') { | ||||
|                 updateNfcData(data.payload); | ||||
|             } else if (data.type === 'writeNfcTag') { | ||||
|                 handleWriteNfcTagResponse(data.success); | ||||
|             } else if (data.type === 'heartbeat') { | ||||
|                 // Optional: Spezifische Behandlung von Heartbeat-Antworten | ||||
|                 // Update status dots | ||||
|                 const bambuDot = document.getElementById('bambuDot'); | ||||
|                 const spoolmanDot = document.getElementById('spoolmanDot'); | ||||
|                 const ramStatus = document.getElementById('ramStatus'); | ||||
|                  | ||||
|                 if (bambuDot) { | ||||
|                     bambuDot.className = 'status-dot ' + (data.bambu_connected ? 'online' : 'offline'); | ||||
|                 } | ||||
|                 if (spoolmanDot) { | ||||
|                     spoolmanDot.className = 'status-dot ' + (data.spoolman_connected ? 'online' : 'offline'); | ||||
|                 } | ||||
|                 if (ramStatus) { | ||||
|                     ramStatus.textContent = `${data.freeHeap}k`; | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|     } catch (error) { | ||||
|         isConnected = false; | ||||
|         updateConnectionStatus(); | ||||
|          | ||||
|         // Nur neue Verbindung versuchen, wenn kein Timer läuft | ||||
|         if (!reconnectTimer) { | ||||
|             reconnectTimer = setTimeout(() => { | ||||
|                 initWebSocket(); | ||||
|             }, RECONNECT_INTERVAL); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| function updateConnectionStatus() { | ||||
|     const statusElement = document.querySelector('.connection-status'); | ||||
|     if (!isConnected) { | ||||
|         statusElement.classList.remove('hidden'); | ||||
|         // Verzögerung hinzufügen, damit die CSS-Transition wirken kann | ||||
|         setTimeout(() => { | ||||
|             statusElement.classList.add('visible'); | ||||
|         }, 10); | ||||
|     } else { | ||||
|         statusElement.classList.remove('visible'); | ||||
|         // Warte auf das Ende der Fade-out Animation bevor wir hidden setzen | ||||
|         setTimeout(() => { | ||||
|             statusElement.classList.add('hidden'); | ||||
|         }, 300); | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Event Listeners | ||||
| document.addEventListener("DOMContentLoaded", function() { | ||||
|     initWebSocket(); | ||||
|      | ||||
|     // Event Listener für Checkbox | ||||
|     document.getElementById("onlyWithoutSmId").addEventListener("change", function() { | ||||
|         const spoolsData = window.getSpoolData(); | ||||
|         window.populateVendorDropdown(spoolsData); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| // Event Listener für Spoolman Events | ||||
| document.addEventListener('spoolDataLoaded', function(event) { | ||||
|     window.populateVendorDropdown(event.detail); | ||||
| }); | ||||
|  | ||||
| document.addEventListener('spoolmanError', function(event) { | ||||
|     showNotification(`Spoolman Error: ${event.detail.message}`, false); | ||||
| }); | ||||
|  | ||||
| document.addEventListener('filamentSelected', function(event) { | ||||
|     updateNfcInfo(); | ||||
|     // Zeige Spool-Buttons wenn ein Filament ausgewählt wurde | ||||
|     const selectedText = document.getElementById("selected-filament").textContent; | ||||
|     updateSpoolButtons(selectedText !== "Please choose..."); | ||||
| }); | ||||
|  | ||||
| // Hilfsfunktion für kontrastreiche Textfarbe | ||||
| function getContrastColor(hexcolor) { | ||||
|     // Konvertiere Hex zu RGB | ||||
|     const r = parseInt(hexcolor.substr(0,2),16); | ||||
|     const g = parseInt(hexcolor.substr(2,2),16); | ||||
|     const b = parseInt(hexcolor.substr(4,2),16); | ||||
|      | ||||
|     // Berechne Helligkeit (YIQ Formel) | ||||
|     const yiq = ((r*299)+(g*587)+(b*114))/1000; | ||||
|      | ||||
|     // Return schwarz oder weiß basierend auf Helligkeit | ||||
|     return (yiq >= 128) ? '#000000' : '#FFFFFF'; | ||||
| } | ||||
|  | ||||
| function updateNfcInfo() { | ||||
|     const selectedText = document.getElementById("selected-filament").textContent; | ||||
|     const nfcInfo = document.getElementById("nfcInfo"); | ||||
|     const writeButton = document.getElementById("writeNfcButton"); | ||||
|  | ||||
|     if (selectedText === "Please choose...") { | ||||
|         nfcInfo.textContent = "No Filament selected"; | ||||
|         nfcInfo.classList.remove("nfc-success", "nfc-error"); | ||||
|         writeButton.classList.add("hidden"); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // Finde die ausgewählte Spule in den Daten | ||||
|     const selectedSpool = spoolsData.find(spool =>  | ||||
|         `${spool.id} | ${spool.filament.name} (${spool.filament.material})` === selectedText | ||||
|     ); | ||||
|  | ||||
|     if (selectedSpool && selectedSpool.extra.nfc_id) { | ||||
|         nfcInfo.textContent = "NFC Tag assigned"; | ||||
|         nfcInfo.classList.add("nfc-success"); | ||||
|         nfcInfo.classList.remove("nfc-error"); | ||||
|     } else { | ||||
|         nfcInfo.textContent = "No NFC-Tag assigned"; | ||||
|         nfcInfo.classList.add("nfc-error"); | ||||
|         nfcInfo.classList.remove("nfc-success"); | ||||
|     } | ||||
|  | ||||
|     if (selectedSpool) { | ||||
|         writeButton.classList.remove("hidden"); | ||||
|     } else { | ||||
|         writeButton.classList.add("hidden"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function displayAmsData(amsData) { | ||||
|     const amsDataContainer = document.getElementById('amsData'); | ||||
|     amsDataContainer.innerHTML = '';  | ||||
|  | ||||
|     amsData.forEach((ams) => { | ||||
|         // Bestimme den Anzeigenamen für das AMS | ||||
|         const amsDisplayName = ams.ams_id === 255 ? 'External Spool' : `AMS ${ams.ams_id}`; | ||||
|          | ||||
|         const trayHTML = ams.tray.map(tray => { | ||||
|             // Prüfe ob überhaupt Daten vorhanden sind | ||||
|             const relevantFields = ['tray_type', 'tray_sub_brands', 'tray_info_idx', 'setting_id']; | ||||
|             const hasAnyContent = relevantFields.some(field =>  | ||||
|                 tray[field] !== null &&  | ||||
|                 tray[field] !== undefined &&  | ||||
|                 tray[field] !== '' && | ||||
|                 tray[field] !== 'null' | ||||
|             ); | ||||
|  | ||||
|             if (!hasAnyContent) { | ||||
|                 return ` | ||||
|                     <div class="tray"> | ||||
|                         <p><b>Tray ${tray.id}</b></p> | ||||
|                         <p>Empty</p> | ||||
|                     </div> | ||||
|                     <hr>`; | ||||
|             } | ||||
|  | ||||
|             // Nur für nicht-leere Trays den Button-HTML erstellen | ||||
|             const buttonHtml = ` | ||||
|                 <button class="spool-button" onclick="handleSpoolIn(${ams.ams_id}, ${tray.id})"  | ||||
|                         style="position: absolute; top: 5px; left: 5px;  | ||||
|                                background: none; border: none; padding: 0;  | ||||
|                                cursor: pointer; display: none;"> | ||||
|                     <img src="spool_in.png" alt="Spool In" style="width: 48px; height: 48px;"> | ||||
|                 </button>`; | ||||
|  | ||||
|             // Generiere den Type mit Color-Box zusammen | ||||
|             const typeWithColor = tray.tray_type ?  | ||||
|                 `<p>Typ: ${tray.tray_type} ${tray.tray_color ? `<span style=" | ||||
|                     background-color: #${tray.tray_color};  | ||||
|                     width: 20px;  | ||||
|                     height: 20px;  | ||||
|                     display: inline-block;  | ||||
|                     vertical-align: middle; | ||||
|                     border: 1px solid #333; | ||||
|                     border-radius: 3px; | ||||
|                     margin-left: 5px;"></span>` : ''}</p>` : ''; | ||||
|  | ||||
|             // Array mit restlichen Tray-Eigenschaften | ||||
|             const trayProperties = [ | ||||
|                 { key: 'tray_sub_brands', label: 'Sub Brands' }, | ||||
|                 { key: 'tray_info_idx', label: 'Filament Index' }, | ||||
|                 { key: 'setting_id', label: 'Setting ID' } | ||||
|             ]; | ||||
|  | ||||
|             // Nur gültige Felder anzeigen | ||||
|             const trayDetails = trayProperties | ||||
|                 .filter(prop =>  | ||||
|                     tray[prop.key] !== null &&  | ||||
|                     tray[prop.key] !== undefined &&  | ||||
|                     tray[prop.key] !== '' && | ||||
|                     tray[prop.key] !== 'null' | ||||
|                 ) | ||||
|                 .map(prop => `<p>${prop.label}: ${tray[prop.key]}</p>`) | ||||
|                 .join(''); | ||||
|  | ||||
|             // Temperaturen nur anzeigen, wenn beide nicht 0 sind | ||||
|             const tempHTML = (tray.nozzle_temp_min > 0 && tray.nozzle_temp_max > 0)  | ||||
|                 ? `<p>Nozzle Temp: ${tray.nozzle_temp_min}°C - ${tray.nozzle_temp_max}°C</p>` | ||||
|                 : ''; | ||||
|  | ||||
|             // Bestimme den Anzeigenamen für das Tray | ||||
|             const trayDisplayName = (ams.ams_id === 255) ? 'External' : `Tray ${tray.id}`; | ||||
|  | ||||
|             return ` | ||||
|                 <div class="tray" ${tray.tray_color ? `style="border-left: 4px solid #${tray.tray_color};"` : 'style="border-left: 4px solid #007bff;"'}> | ||||
|                     <div style="position: relative;"> | ||||
|                         ${buttonHtml} | ||||
|                         <p><b>${trayDisplayName}</b></p> | ||||
|                         ${typeWithColor} | ||||
|                         ${trayDetails} | ||||
|                         ${tempHTML} | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <hr>`; | ||||
|         }).join(''); | ||||
|  | ||||
|         const amsInfo = ` | ||||
|             <div class="feature"> | ||||
|                 <h3>${amsDisplayName}:</h3> | ||||
|                 <div id="trayContainer"> | ||||
|                     ${trayHTML} | ||||
|                 </div> | ||||
|             </div>`; | ||||
|          | ||||
|         amsDataContainer.innerHTML += amsInfo; | ||||
|     }); | ||||
| } | ||||
|  | ||||
| // Neue Funktion zum Anzeigen/Ausblenden der Spool-Buttons | ||||
| function updateSpoolButtons(show) { | ||||
|     const spoolButtons = document.querySelectorAll('.spool-button'); | ||||
|     spoolButtons.forEach(button => { | ||||
|         button.style.display = show ? 'block' : 'none'; | ||||
|     }); | ||||
| } | ||||
|  | ||||
| // Neue Funktion zum Behandeln des Spool-In-Klicks | ||||
| function handleSpoolIn(amsId, trayId) { | ||||
|     // Prüfe WebSocket Verbindung zuerst | ||||
|     if (!socket || socket.readyState !== WebSocket.OPEN) { | ||||
|         showNotification("No active WebSocket connection!", false); | ||||
|         console.error("WebSocket not connected"); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // Hole das ausgewählte Filament | ||||
|     const selectedText = document.getElementById("selected-filament").textContent; | ||||
|     if (selectedText === "Please choose...") { | ||||
|         showNotification("Choose Filament first", false); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // Finde die ausgewählte Spule in den Daten | ||||
|     const selectedSpool = spoolsData.find(spool =>  | ||||
|         `${spool.id} | ${spool.filament.name} (${spool.filament.material})` === selectedText | ||||
|     ); | ||||
|  | ||||
|     if (!selectedSpool) { | ||||
|         showNotification("Selected Spool not found", false); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // Temperaturwerte extrahieren | ||||
|     let minTemp = "175"; | ||||
|     let maxTemp = "275"; | ||||
|      | ||||
|     if (Array.isArray(selectedSpool.filament.nozzle_temperature) &&  | ||||
|         selectedSpool.filament.nozzle_temperature.length >= 2) { | ||||
|         minTemp = selectedSpool.filament.nozzle_temperature[0]; | ||||
|         maxTemp = selectedSpool.filament.nozzle_temperature[1]; | ||||
|     } | ||||
|  | ||||
|     // Erstelle Payload | ||||
|     const payload = { | ||||
|         type: 'setBambuSpool', | ||||
|         payload: { | ||||
|             amsId: amsId, | ||||
|             trayId: trayId, | ||||
|             color: selectedSpool.filament.color_hex || "FFFFFF", | ||||
|             nozzle_temp_min: parseInt(minTemp), | ||||
|             nozzle_temp_max: parseInt(maxTemp), | ||||
|             type: selectedSpool.filament.material, | ||||
|             brand: selectedSpool.filament.vendor.name | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     // Debug logging | ||||
|     console.log("Sende WebSocket Nachricht:", payload); | ||||
|  | ||||
|     try { | ||||
|         socket.send(JSON.stringify(payload)); | ||||
|         showNotification(`Spool set in AMS ${amsId} Tray ${trayId}. Pls wait`, true); | ||||
|     } catch (error) { | ||||
|         console.error("Fehler beim Senden der WebSocket Nachricht:", error); | ||||
|         showNotification("Fehler beim Senden der Daten", false); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function updateNfcStatusIndicator(data) { | ||||
|     const indicator = document.getElementById('nfcStatusIndicator'); | ||||
|      | ||||
|     if (data.found === 0) { | ||||
|         // Kein NFC Tag gefunden | ||||
|         indicator.className = 'status-circle'; | ||||
|     } else if (data.found === 1) { | ||||
|         // NFC Tag erfolgreich gelesen | ||||
|         indicator.className = 'status-circle success'; | ||||
|     } else { | ||||
|         // Fehler beim Lesen | ||||
|         indicator.className = 'status-circle error'; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function updateNfcData(data) { | ||||
|     // Den Container für den NFC Status finden | ||||
|     const nfcStatusContainer = document.querySelector('.nfc-status-display'); | ||||
|      | ||||
|     // Bestehende Daten-Anzeige entfernen falls vorhanden | ||||
|     const existingData = nfcStatusContainer.querySelector('.nfc-data'); | ||||
|     if (existingData) { | ||||
|         existingData.remove(); | ||||
|     } | ||||
|  | ||||
|     // Neues div für die Datenanzeige erstellen | ||||
|     const nfcDataDiv = document.createElement('div'); | ||||
|     nfcDataDiv.className = 'nfc-data'; | ||||
|  | ||||
|     // Wenn ein Fehler vorliegt oder keine Daten vorhanden sind | ||||
|     if (data.error || data.info || !data || Object.keys(data).length === 0) { | ||||
|         // Zeige Fehlermeldung oder leere Nachricht | ||||
|         if (data.error || data.info) { | ||||
|             if (data.error) { | ||||
|                 nfcDataDiv.innerHTML = ` | ||||
|                     <div class="error-message" style="margin-top: 10px; color: #dc3545;"> | ||||
|                         <p><strong>Error:</strong> ${data.error}</p> | ||||
|                     </div>`; | ||||
|             } else { | ||||
|                 nfcDataDiv.innerHTML = ` | ||||
|                     <div class="info-message" style="margin-top: 10px; color:rgb(18, 210, 0);"> | ||||
|                         <p><strong>Info:</strong> ${data.info}</p> | ||||
|                     </div>`; | ||||
|             } | ||||
|  | ||||
|         } else { | ||||
|             nfcDataDiv.innerHTML = '<div style="margin-top: 10px;"></div>'; | ||||
|         } | ||||
|         nfcStatusContainer.appendChild(nfcDataDiv); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // HTML für die Datenanzeige erstellen | ||||
|     let html = ` | ||||
|         <div style="margin-top: 10px;"> | ||||
|             <p><strong>Brand:</strong> ${data.brand || 'N/A'}</p> | ||||
|             <p><strong>Type:</strong> ${data.type || 'N/A'} ${data.color_hex ? `<span style=" | ||||
|                 background-color: #${data.color_hex};  | ||||
|                 width: 20px;  | ||||
|                 height: 20px;  | ||||
|                 display: inline-block;  | ||||
|                 vertical-align: middle; | ||||
|                 border: 1px solid #333; | ||||
|                 border-radius: 3px; | ||||
|                 margin-left: 5px; | ||||
|             "></span>` : ''}</p> | ||||
|     `; | ||||
|  | ||||
|     // Spoolman ID anzeigen | ||||
|     html += `<p><strong>Spoolman ID:</strong> ${data.sm_id || 'No Spoolman ID'}</p>`; | ||||
|  | ||||
|     // Nur wenn eine sm_id vorhanden ist, aktualisiere die Dropdowns | ||||
|     if (data.sm_id) { | ||||
|         const matchingSpool = spoolsData.find(spool => spool.id === parseInt(data.sm_id)); | ||||
|         if (matchingSpool) { | ||||
|             // Zuerst Hersteller-Dropdown aktualisieren | ||||
|             document.getElementById("vendorSelect").value = matchingSpool.filament.vendor.id; | ||||
|              | ||||
|             // Dann Filament-Dropdown aktualisieren und Spule auswählen | ||||
|             updateFilamentDropdown(); | ||||
|             setTimeout(() => { | ||||
|                 // Warte kurz bis das Dropdown aktualisiert wurde | ||||
|                 selectFilament(matchingSpool); | ||||
|             }, 100); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     html += '</div>'; | ||||
|     nfcDataDiv.innerHTML = html; | ||||
|  | ||||
|      | ||||
|     // Neues div zum Container hinzufügen | ||||
|     nfcStatusContainer.appendChild(nfcDataDiv); | ||||
| } | ||||
|  | ||||
| function writeNfcTag() { | ||||
|     const selectedText = document.getElementById("selected-filament").textContent; | ||||
|     if (selectedText === "Please choose...") { | ||||
|         alert('Please select a Spool first.'); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const spoolsData = window.getSpoolData(); | ||||
|     const selectedSpool = spoolsData.find(spool =>  | ||||
|         `${spool.id} | ${spool.filament.name} (${spool.filament.material})` === selectedText | ||||
|     ); | ||||
|  | ||||
|     if (!selectedSpool) { | ||||
|         alert('Ausgewählte Spule konnte nicht gefunden werden.'); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // Temperaturwerte korrekt extrahieren | ||||
|     let minTemp = "175"; | ||||
|     let maxTemp = "275"; | ||||
|      | ||||
|     if (Array.isArray(selectedSpool.filament.nozzle_temperature) &&  | ||||
|         selectedSpool.filament.nozzle_temperature.length >= 2) { | ||||
|         minTemp = String(selectedSpool.filament.nozzle_temperature[0]); | ||||
|         maxTemp = String(selectedSpool.filament.nozzle_temperature[1]); | ||||
|     } | ||||
|  | ||||
|     // Erstelle das NFC-Datenpaket mit korrekten Datentypen | ||||
|     const nfcData = { | ||||
|         version: "2.0", | ||||
|         protocol: "openspool", | ||||
|         color_hex: selectedSpool.filament.color_hex || "FFFFFF", | ||||
|         type: selectedSpool.filament.material, | ||||
|         min_temp: minTemp, | ||||
|         max_temp: maxTemp, | ||||
|         brand: selectedSpool.filament.vendor.name, | ||||
|         sm_id: String(selectedSpool.id) // Konvertiere zu String | ||||
|     }; | ||||
|  | ||||
|     if (socket?.readyState === WebSocket.OPEN) { | ||||
|         const writeButton = document.getElementById("writeNfcButton"); | ||||
|         writeButton.classList.add("writing"); | ||||
|         writeButton.textContent = "Writing"; | ||||
|         socket.send(JSON.stringify({ | ||||
|             type: 'writeNfcTag', | ||||
|             payload: nfcData | ||||
|         })); | ||||
|     } else { | ||||
|         alert('Not connected to Server. Please check connection.'); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function handleWriteNfcTagResponse(success) { | ||||
|     const writeButton = document.getElementById("writeNfcButton"); | ||||
|     writeButton.classList.remove("writing"); | ||||
|     writeButton.classList.add(success ? "success" : "error"); | ||||
|     writeButton.textContent = success ? "Write success" : "Write failed"; | ||||
|  | ||||
|     setTimeout(() => { | ||||
|         writeButton.classList.remove("success", "error"); | ||||
|         writeButton.textContent = "Write Tag"; | ||||
|     }, 5000); | ||||
| } | ||||
|  | ||||
| function showNotification(message, isSuccess) { | ||||
|     const notification = document.createElement('div'); | ||||
|     notification.className = `notification ${isSuccess ? 'success' : 'error'}`; | ||||
|     notification.textContent = message; | ||||
|     document.body.appendChild(notification); | ||||
|  | ||||
|     // Nach 3 Sekunden ausblenden | ||||
|     setTimeout(() => { | ||||
|         notification.classList.add('fade-out'); | ||||
|         setTimeout(() => { | ||||
|             notification.remove(); | ||||
|         }, 300); | ||||
|     }, 3000); | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								html/spool_in.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								html/spool_in.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 22 KiB | 
							
								
								
									
										80
									
								
								html/spoolman.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								html/spoolman.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| {{header}} | ||||
|     <script> | ||||
|         window.onload = function() { | ||||
|             if (spoolmanUrl && spoolmanUrl.trim() !== "") { | ||||
|                 document.getElementById('spoolmanUrl').value = spoolmanUrl; | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         function checkSpoolmanInstance() { | ||||
|             const url = document.getElementById('spoolmanUrl').value; | ||||
|             fetch(`/api/checkSpoolman?url=${encodeURIComponent(url)}`) | ||||
|                 .then(response => response.json()) | ||||
|                 .then(data => { | ||||
|                     if (data.healthy) { | ||||
|                         document.getElementById('statusMessage').innerText = 'Spoolman-Instance is availabe and healthy!'; | ||||
|                     } else { | ||||
|                         document.getElementById('statusMessage').innerText = 'Spoolman-Instance not available.'; | ||||
|                     } | ||||
|                 }) | ||||
|                 .catch(error => { | ||||
|                     document.getElementById('statusMessage').innerText = 'Error while connecting to Spoolman-Instance: ' + error.message; | ||||
|                 }); | ||||
|         } | ||||
|  | ||||
|         function saveBambuCredentials() { | ||||
|             const ip = document.getElementById('bambuIp').value; | ||||
|             const serial = document.getElementById('bambuSerial').value; | ||||
|             const code = document.getElementById('bambuCode').value; | ||||
|  | ||||
|             fetch(`/api/bambu?bambu_ip=${encodeURIComponent(ip)}&bambu_serialnr=${encodeURIComponent(serial)}&bambu_accesscode=${encodeURIComponent(code)}`) | ||||
|                 .then(response => response.json()) | ||||
|                 .then(data => { | ||||
|                     if (data.healthy) { | ||||
|                         document.getElementById('bambuStatusMessage').innerText = 'Bambu Credentials saved!'; | ||||
|                         // Erstelle und zeige den Reboot-Button | ||||
|                         const rebootBtn = document.createElement('button'); | ||||
|                         rebootBtn.innerText = 'Reboot now'; | ||||
|                         rebootBtn.className = 'reboot-button'; | ||||
|                         rebootBtn.onclick = () => window.location.href = '/reboot'; | ||||
|                         document.getElementById('bambuStatusMessage').appendChild(rebootBtn); | ||||
|                     } else { | ||||
|                         document.getElementById('bambuStatusMessage').innerText = 'Error while saving Bambu Credentials.'; | ||||
|                     } | ||||
|                 }) | ||||
|                 .catch(error => { | ||||
|                     document.getElementById('bambuStatusMessage').innerText = 'Error while saving: ' + error.message; | ||||
|                 }); | ||||
|         } | ||||
|     </script> | ||||
|     <script> | ||||
|         var spoolmanUrl = "{{spoolmanUrl}}"; | ||||
|     </script> | ||||
|      | ||||
|     <div class="content"> | ||||
|         <h1>Spoolman API URL / Bambu Credentials</h1> | ||||
|         <label for="spoolmanUrl">Set URL/IP to your Spoolman-Instanz:</label> | ||||
|         <input type="text" id="spoolmanUrl" placeholder="http://ip-or-url-of-your-spoolman-instanz:port"> | ||||
|         <button onclick="checkSpoolmanInstance()">Save Spoolman URL</button> | ||||
|         <p id="statusMessage"></p> | ||||
|  | ||||
|         <h2>Bambu Lab Printer Credentials</h2> | ||||
|         <div class="bambu-settings"> | ||||
|             <div class="input-group"> | ||||
|                 <label for="bambuIp">Bambu Drucker IP-Adresse:</label> | ||||
|                 <input type="text" id="bambuIp" placeholder="192.168.1.xxx" value="{{bambuIp}}"> | ||||
|             </div> | ||||
|             <div class="input-group"> | ||||
|                 <label for="bambuSerial">Drucker Seriennummer:</label> | ||||
|                 <input type="text" id="bambuSerial" placeholder="BBLXXXXXXXX" value="{{bambuSerial}}"> | ||||
|             </div> | ||||
|             <div class="input-group"> | ||||
|                 <label for="bambuCode">Access Code:</label> | ||||
|                 <input type="text" id="bambuCode" placeholder="Access Code vom Drucker" value="{{bambuCode}}"> | ||||
|             </div> | ||||
|             <button onclick="saveBambuCredentials()">Save Bambu Credentials</button> | ||||
|             <p id="bambuStatusMessage"></p> | ||||
|         </div> | ||||
|     </div> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										308
									
								
								html/spoolman.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								html/spoolman.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,308 @@ | ||||
| // Globale Variablen | ||||
| let spoolmanUrl = ''; | ||||
| let spoolsData = []; | ||||
|  | ||||
| // Hilfsfunktionen für Datenmanipulation | ||||
| function processSpoolData(data) { | ||||
|     return data.map(spool => ({ | ||||
|         id: spool.id, | ||||
|         remaining_weight: spool.remaining_weight, | ||||
|         remaining_length: spool.remaining_length, | ||||
|         filament: spool.filament, | ||||
|         extra: spool.extra | ||||
|     })); | ||||
| } | ||||
|  | ||||
| // Dropdown-Funktionen | ||||
| function populateVendorDropdown(data, selectedSmId = null) { | ||||
|     const vendorSelect = document.getElementById("vendorSelect"); | ||||
|     if (!vendorSelect) { | ||||
|         console.error('vendorSelect Element nicht gefunden'); | ||||
|         return; | ||||
|     } | ||||
|     const onlyWithoutSmId = document.getElementById("onlyWithoutSmId"); | ||||
|     if (!onlyWithoutSmId) { | ||||
|         console.error('onlyWithoutSmId Element nicht gefunden'); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // Separate Objekte für alle Hersteller und gefilterte Hersteller | ||||
|     const allVendors = {}; | ||||
|     const filteredVendors = {}; | ||||
|  | ||||
|     vendorSelect.innerHTML = '<option value="">Bitte wählen...</option>'; | ||||
|  | ||||
|     let vendorIdToSelect = null; | ||||
|     let totalSpools = 0; | ||||
|     let spoolsWithoutTag = 0; | ||||
|     let totalWeight = 0; | ||||
|     let totalLength = 0; | ||||
|     // Neues Objekt für Material-Gruppierung | ||||
|     const materials = {}; | ||||
|  | ||||
|     data.forEach(spool => { | ||||
|         if (!spool.filament || !spool.filament.vendor) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         totalSpools++; | ||||
|          | ||||
|         // Material zählen und gruppieren | ||||
|         if (spool.filament.material) { | ||||
|             const material = spool.filament.material.toUpperCase(); // Normalisierung | ||||
|             materials[material] = (materials[material] || 0) + 1; | ||||
|         } | ||||
|  | ||||
|         // Addiere Gewicht und Länge | ||||
|         if (spool.remaining_weight) { | ||||
|             totalWeight += spool.remaining_weight; | ||||
|         } | ||||
|         if (spool.remaining_length) { | ||||
|             totalLength += spool.remaining_length; | ||||
|         } | ||||
|  | ||||
|         console.log("Länge gesamt: " + spool.remaining_length); | ||||
|         console.log("Gewicht gesamt" + spool.remaining_weight); | ||||
|  | ||||
|         const vendor = spool.filament.vendor; | ||||
|          | ||||
|         const hasValidNfcId = spool.extra &&  | ||||
|                              spool.extra.nfc_id &&  | ||||
|                              spool.extra.nfc_id !== '""' &&  | ||||
|                              spool.extra.nfc_id !== '"\\"\\"\\""'; | ||||
|          | ||||
|         if (!hasValidNfcId) { | ||||
|             spoolsWithoutTag++; | ||||
|         } | ||||
|  | ||||
|         // Alle Hersteller sammeln | ||||
|         if (!allVendors[vendor.id]) { | ||||
|             allVendors[vendor.id] = vendor.name; | ||||
|         } | ||||
|  | ||||
|         // Gefilterte Hersteller für Dropdown | ||||
|         if (!filteredVendors[vendor.id]) { | ||||
|             if (!onlyWithoutSmId.checked || !hasValidNfcId) { | ||||
|                 filteredVendors[vendor.id] = vendor.name; | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     // Dropdown mit gefilterten Herstellern befüllen | ||||
|     Object.entries(filteredVendors).forEach(([id, name]) => { | ||||
|         const option = document.createElement("option"); | ||||
|         option.value = id; | ||||
|         option.textContent = name; | ||||
|         vendorSelect.appendChild(option); | ||||
|     }); | ||||
|  | ||||
|     document.getElementById("totalSpools").textContent = totalSpools; | ||||
|     document.getElementById("spoolsWithoutTag").textContent = spoolsWithoutTag; | ||||
|     // Zeige die Gesamtzahl aller Hersteller an | ||||
|     document.getElementById("totalVendors").textContent = Object.keys(allVendors).length; | ||||
|      | ||||
|     // Neue Statistiken hinzufügen | ||||
|     document.getElementById("totalWeight").textContent = (totalWeight / 1000).toFixed(2); | ||||
|     document.getElementById("totalLength").textContent = (totalLength / 1000).toFixed(2); | ||||
|  | ||||
|     // Material-Statistiken zum DOM hinzufügen | ||||
|     const materialsList = document.getElementById("materialsList"); | ||||
|     materialsList.innerHTML = ''; | ||||
|     Object.entries(materials) | ||||
|         .sort(([,a], [,b]) => b - a) // Sortiere nach Anzahl absteigend | ||||
|         .forEach(([material, count]) => { | ||||
|             const li = document.createElement("li"); | ||||
|             li.textContent = `${material}: ${count} ${count === 1 ? 'Spule' : 'Spulen'}`; | ||||
|             materialsList.appendChild(li); | ||||
|         }); | ||||
|  | ||||
|     if (vendorIdToSelect) { | ||||
|         vendorSelect.value = vendorIdToSelect; | ||||
|         updateFilamentDropdown(selectedSmId); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function updateFilamentDropdown(selectedSmId = null) { | ||||
|     const vendorId = document.getElementById("vendorSelect").value; | ||||
|     const dropdownContentInner = document.getElementById("filament-dropdown-content"); | ||||
|     const filamentSection = document.getElementById("filamentSection"); | ||||
|     const onlyWithoutSmId = document.getElementById("onlyWithoutSmId").checked; | ||||
|     const selectedText = document.getElementById("selected-filament"); | ||||
|     const selectedColor = document.getElementById("selected-color"); | ||||
|  | ||||
|     dropdownContentInner.innerHTML = ''; | ||||
|     selectedText.textContent = "Bitte wählen..."; | ||||
|     selectedColor.style.backgroundColor = '#FFFFFF'; | ||||
|  | ||||
|     if (vendorId) { | ||||
|         const filteredFilaments = spoolsData.filter(spool => { | ||||
|             const hasValidNfcId = spool.extra &&  | ||||
|                                  spool.extra.nfc_id &&  | ||||
|                                  spool.extra.nfc_id !== '""' &&  | ||||
|                                  spool.extra.nfc_id !== '"\\"\\"\\""'; | ||||
|              | ||||
|             return spool.filament.vendor.id == vendorId &&  | ||||
|                    (!onlyWithoutSmId || !hasValidNfcId); | ||||
|         }); | ||||
|  | ||||
|         filteredFilaments.forEach(spool => { | ||||
|             const option = document.createElement("div"); | ||||
|             option.className = "dropdown-option"; | ||||
|             option.setAttribute("data-value", spool.filament.id); | ||||
|             option.setAttribute("data-nfc-id", spool.extra.nfc_id || ""); | ||||
|              | ||||
|             const colorHex = spool.filament.color_hex || 'FFFFFF'; | ||||
|             option.innerHTML = ` | ||||
|                 <div class="option-color" style="background-color: #${colorHex}"></div> | ||||
|                 <span>${spool.id} | ${spool.filament.name} (${spool.filament.material})</span> | ||||
|             `; | ||||
|              | ||||
|             option.onclick = () => selectFilament(spool); | ||||
|             dropdownContentInner.appendChild(option); | ||||
|         }); | ||||
|  | ||||
|         filamentSection.classList.remove("hidden"); | ||||
|     } else { | ||||
|         filamentSection.classList.add("hidden"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function selectFilament(spool) { | ||||
|     const selectedColor = document.getElementById("selected-color"); | ||||
|     const selectedText = document.getElementById("selected-filament"); | ||||
|     const dropdownContent = document.getElementById("filament-dropdown-content"); | ||||
|      | ||||
|     selectedColor.style.backgroundColor = `#${spool.filament.color_hex || 'FFFFFF'}`; | ||||
|     selectedText.textContent = `${spool.id} | ${spool.filament.name} (${spool.filament.material})`; | ||||
|     dropdownContent.classList.remove("show"); | ||||
|      | ||||
|     document.dispatchEvent(new CustomEvent('filamentSelected', {  | ||||
|         detail: spool  | ||||
|     })); | ||||
| } | ||||
|  | ||||
| // Initialisierung und Event-Handler | ||||
| async function initSpoolman() { | ||||
|     try { | ||||
|         const response = await fetch('/api/url'); | ||||
|         if (!response.ok) { | ||||
|             throw new Error(`HTTP error! status: ${response.status}`); | ||||
|         } | ||||
|          | ||||
|         const data = await response.json(); | ||||
|         if (!data.spoolman_url) { | ||||
|             throw new Error('spoolman_url nicht in der Antwort gefunden'); | ||||
|         } | ||||
|          | ||||
|         spoolmanUrl = data.spoolman_url; | ||||
|          | ||||
|         const fetchedData = await fetchSpoolData(); | ||||
|         spoolsData = processSpoolData(fetchedData); | ||||
|          | ||||
|         document.dispatchEvent(new CustomEvent('spoolDataLoaded', {  | ||||
|             detail: spoolsData  | ||||
|         })); | ||||
|     } catch (error) { | ||||
|         console.error('Fehler beim Initialisieren von Spoolman:', error); | ||||
|         document.dispatchEvent(new CustomEvent('spoolmanError', {  | ||||
|             detail: { message: error.message }  | ||||
|         })); | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function fetchSpoolData() { | ||||
|     try { | ||||
|         if (!spoolmanUrl) { | ||||
|             throw new Error('Spoolman URL ist nicht initialisiert'); | ||||
|         } | ||||
|          | ||||
|         const response = await fetch(`${spoolmanUrl}/api/v1/spool`); | ||||
|         if (!response.ok) { | ||||
|             throw new Error(`HTTP error! status: ${response.status}`); | ||||
|         } | ||||
|          | ||||
|         const data = await response.json(); | ||||
|         return data; | ||||
|     } catch (error) { | ||||
|         console.error('Fehler beim Abrufen der Spulen-Daten:', error); | ||||
|         return []; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* | ||||
| // Exportiere Funktionen | ||||
| window.getSpoolData = () => spoolsData; | ||||
| window.reloadSpoolData = initSpoolman; | ||||
| window.populateVendorDropdown = populateVendorDropdown; | ||||
| window.updateFilamentDropdown = updateFilamentDropdown; | ||||
| window.toggleFilamentDropdown = () => { | ||||
|     const content = document.getElementById("filament-dropdown-content"); | ||||
|     content.classList.toggle("show"); | ||||
| }; | ||||
| */ | ||||
|  | ||||
| // Event Listener | ||||
| document.addEventListener('DOMContentLoaded', () => { | ||||
|     initSpoolman(); | ||||
|      | ||||
|     const vendorSelect = document.getElementById('vendorSelect'); | ||||
|     if (vendorSelect) { | ||||
|         vendorSelect.addEventListener('change', () => updateFilamentDropdown()); | ||||
|     } | ||||
|      | ||||
|     const onlyWithoutSmId = document.getElementById('onlyWithoutSmId'); | ||||
|     if (onlyWithoutSmId) { | ||||
|         onlyWithoutSmId.addEventListener('change', () => { | ||||
|             populateVendorDropdown(spoolsData); | ||||
|             updateFilamentDropdown(); | ||||
|         }); | ||||
|     } | ||||
|      | ||||
|     document.addEventListener('spoolDataLoaded', (event) => { | ||||
|         populateVendorDropdown(event.detail); | ||||
|     }); | ||||
|      | ||||
|     window.onclick = function(event) { | ||||
|         if (!event.target.closest('.custom-dropdown')) { | ||||
|             const dropdowns = document.getElementsByClassName("dropdown-content"); | ||||
|             for (let dropdown of dropdowns) { | ||||
|                 dropdown.classList.remove("show"); | ||||
|             } | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     const refreshButton = document.getElementById('refreshSpoolman'); | ||||
|     if (refreshButton) { | ||||
|         refreshButton.addEventListener('click', async () => { | ||||
|             try { | ||||
|                 refreshButton.disabled = true; | ||||
|                 refreshButton.textContent = 'Wird aktualisiert...'; | ||||
|                 await initSpoolman(); | ||||
|                 refreshButton.textContent = 'Refresh Spoolman'; | ||||
|             } finally { | ||||
|                 refreshButton.disabled = false; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| // Exportiere Funktionen | ||||
| window.getSpoolData = () => spoolsData; | ||||
| window.setSpoolData = (data) => { spoolsData = data; }; | ||||
| window.reloadSpoolData = initSpoolman; | ||||
| window.populateVendorDropdown = populateVendorDropdown; | ||||
| window.updateFilamentDropdown = updateFilamentDropdown; | ||||
| window.toggleFilamentDropdown = () => { | ||||
|     const content = document.getElementById("filament-dropdown-content"); | ||||
|     content.classList.toggle("show"); | ||||
| }; | ||||
|  | ||||
| // Event Listener für Click außerhalb Dropdown | ||||
| window.onclick = function(event) { | ||||
|     if (!event.target.closest('.custom-dropdown')) { | ||||
|         const dropdowns = document.getElementsByClassName("dropdown-content"); | ||||
|         for (let dropdown of dropdowns) { | ||||
|             dropdown.classList.remove("show"); | ||||
|         } | ||||
|     } | ||||
| }; | ||||
							
								
								
									
										901
									
								
								html/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										901
									
								
								html/style.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,901 @@ | ||||
| /* Allgemeine Stile */ | ||||
| body {  | ||||
|     font-family: Arial, sans-serif;  | ||||
|     margin: 0;  | ||||
|     padding: 0;  | ||||
|     background-color: #f8f9fa;  | ||||
|     color: #333; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     min-height: 100vh; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .logo { | ||||
|     height: 40px;  /* Anpassen an die Navbar-Höhe */ | ||||
|     width: auto; | ||||
|     margin-right: 15px; | ||||
|     margin-left: 10px; | ||||
| } | ||||
|  | ||||
| /* Navigationsleiste */ | ||||
| .navbar {  | ||||
|     background-color: #007bff;  | ||||
|     width: 100%; | ||||
|     display: flex; | ||||
|     justify-content: center; /* Zentriert die Navigation */ | ||||
|     padding: 10px 0; | ||||
|     box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | ||||
|     position: fixed; | ||||
|     top: 0; | ||||
|     left: 0; | ||||
|     z-index: 1000; | ||||
| } | ||||
|  | ||||
| .navbar a {  | ||||
|     display: inline-block;  | ||||
|     color: white;  | ||||
|     text-align: center;  | ||||
|     padding: 14px 20px;  | ||||
|     text-decoration: none;  | ||||
|     font-weight: bold; | ||||
|     transition: background 0.3s, color 0.3s; | ||||
|     cursor: pointer !important; /* Wichtig: cursor-Definition für Nav-Links */ | ||||
| } | ||||
|  | ||||
| .navbar a:hover {  | ||||
|     background-color: #0056b3;  | ||||
|     color: #fff; | ||||
|     cursor: pointer !important; | ||||
| } | ||||
|  | ||||
| /* Inhalt */ | ||||
| .container {  | ||||
|     padding: 20px;  | ||||
|     width: 100%; | ||||
|     max-width: none; | ||||
|     background: white;  | ||||
|     border-radius: 8px;  | ||||
|     box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | ||||
|     margin-top: 20px; /* Platz für die fixe Navbar */ | ||||
| } | ||||
|  | ||||
| /* Überschriften */ | ||||
| h1 {  | ||||
|     color: #007bff;  | ||||
|     text-align: center;  | ||||
| } | ||||
|  | ||||
| h3 { | ||||
|     color: #007bff; | ||||
|     font-size: 24px; | ||||
|     margin-top: 5px; | ||||
|     margin-bottom: 5px; | ||||
|     font-weight: bold; | ||||
| } | ||||
|  | ||||
| /* Formulare */ | ||||
| form {  | ||||
|     display: flex;  | ||||
|     flex-direction: column;  | ||||
|     gap: 10px;  | ||||
|     padding: 20px; | ||||
| } | ||||
|  | ||||
| label {  | ||||
|     font-weight: bold;  | ||||
| } | ||||
|  | ||||
| input[type="text"], input[type="submit"] {  | ||||
|     padding: 10px;  | ||||
|     border: 1px solid #ccc;  | ||||
|     border-radius: 5px;  | ||||
|     font-size: 16px;  | ||||
| } | ||||
|  | ||||
| input[type="text"]:focus {  | ||||
|     border-color: #007bff;  | ||||
|     outline: none;  | ||||
| } | ||||
|  | ||||
| input[type="submit"] {  | ||||
|     background-color: #007bff;  | ||||
|     color: white;  | ||||
|     border: none;  | ||||
|     cursor: pointer;  | ||||
|     transition: background 0.3s; | ||||
| } | ||||
|  | ||||
| input[type="submit"]:hover {  | ||||
|     background-color: #0056b3;  | ||||
| } | ||||
|  | ||||
| /* Buttons */ | ||||
| button {  | ||||
|     padding: 10px 15px;  | ||||
|     border: none;  | ||||
|     border-radius: 5px;  | ||||
|     background-color: #007bff;  | ||||
|     color: white;  | ||||
|     font-size: 16px;  | ||||
|     cursor: pointer;  | ||||
|     transition: background 0.3s; | ||||
| } | ||||
|  | ||||
| button:hover {  | ||||
|     background-color: #0056b3;  | ||||
| } | ||||
|  | ||||
| /* Statusnachricht */ | ||||
| #statusMessage {  | ||||
|     margin-top: 10px;  | ||||
|     padding: 10px;  | ||||
|     border-radius: 5px;  | ||||
|     background-color: #8cc4fd;  | ||||
|     text-align: center;  | ||||
|     font-weight: bold; | ||||
| } | ||||
|  | ||||
| .features { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     margin-top: 30px; | ||||
|     text-align: left; | ||||
| } | ||||
| .feature { | ||||
|     flex: 1; | ||||
|     padding: 20px; | ||||
|     background-color: #f9f9f9; | ||||
|     border-radius: 8px; | ||||
|     margin: 0 10px; | ||||
|     box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05); | ||||
| } | ||||
| .feature h3 { | ||||
|     font-size: 1.4rem; | ||||
|     margin-bottom: 10px; | ||||
|     color: #007bff; | ||||
| } | ||||
| .feature p { | ||||
|     font-size: 1rem; | ||||
|     color: #555; | ||||
| } | ||||
|  | ||||
| p { | ||||
|     font-size: 1rem; | ||||
|     color: #555; | ||||
| } | ||||
|  | ||||
| a { | ||||
|     color: #007bff; | ||||
|     text-decoration: none; | ||||
|     font-weight: bold; | ||||
|     cursor: pointer; | ||||
| } | ||||
| a:hover { | ||||
|     text-decoration: underline; | ||||
| } | ||||
|  | ||||
| /* Karten-Stil für optische Trennung */ | ||||
| .card { | ||||
|     background: #f9f9f9; | ||||
|     padding: 15px; | ||||
|     margin: 20px 0; | ||||
|     border-radius: 8px; | ||||
|     box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||||
| } | ||||
|  | ||||
| /* Versteckte Elemente */ | ||||
| .hidden { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| /* Dropdown-Stil */ | ||||
| .styled-select { | ||||
|     width: 100%; | ||||
|     padding: 12px 15px; | ||||
|     font-size: 16px; | ||||
|     border: 2px solid #e0e0e0; | ||||
|     border-radius: 8px; | ||||
|     background-color: #fff; | ||||
|     cursor: pointer; | ||||
|     appearance: none; | ||||
|     -webkit-appearance: none; | ||||
|     -moz-appearance: none; | ||||
|     background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23007bff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); | ||||
|     background-repeat: no-repeat; | ||||
|     background-position: right 15px center; | ||||
|     background-size: 15px; | ||||
|     transition: all 0.3s ease; | ||||
|     box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | ||||
| } | ||||
|  | ||||
| .styled-select:hover { | ||||
|     border-color: #007bff; | ||||
|     box-shadow: 0 3px 6px rgba(0, 123, 255, 0.1); | ||||
| } | ||||
|  | ||||
| .styled-select:focus { | ||||
|     border-color: #007bff; | ||||
|     outline: none; | ||||
|     box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25); | ||||
| } | ||||
|  | ||||
| .styled-select:disabled { | ||||
|     background-color: #f5f5f5; | ||||
|     cursor: not-allowed; | ||||
|     opacity: 0.7; | ||||
| } | ||||
|  | ||||
| /* NFC-Status */ | ||||
| .nfc-status { | ||||
|     font-weight: bold; | ||||
|     margin-top: 10px; | ||||
| } | ||||
|  | ||||
| .nfc-success { | ||||
|     color: green; | ||||
| } | ||||
|  | ||||
| .nfc-error { | ||||
|     color: red; | ||||
| } | ||||
|  | ||||
| /* Füge diese neuen Styles zu deiner style.css hinzu */ | ||||
|  | ||||
| .three-column-layout { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     gap: 20px; | ||||
|     margin-top: 20px; | ||||
|     width: 100%; | ||||
| } | ||||
|  | ||||
| .column { | ||||
|     flex: 1; | ||||
|     min-width: 0; /* Verhindert Überlauf bei flex-Elementen */ | ||||
| } | ||||
|  | ||||
| .feature-box { | ||||
|     background: white; | ||||
|     padding: 5px 20px 20px 20px; | ||||
|     border-radius: 8px; | ||||
|     box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||||
|     margin-bottom: 20px; | ||||
| } | ||||
|  | ||||
| .feature-box h2 { | ||||
|     color: #007bff; | ||||
|     font-size: 1.4rem; | ||||
|     margin-bottom: 15px; | ||||
| } | ||||
|  | ||||
| .feature-box ul { | ||||
|     list-style: none; | ||||
|     padding: 0; | ||||
|     margin: 0; | ||||
| } | ||||
|  | ||||
| .feature-box ul li { | ||||
|     padding: 8px 5px 5px 5px; | ||||
|     border-bottom: 1px solid #eee; | ||||
| } | ||||
|  | ||||
| .content { | ||||
|     width: 95%; | ||||
|     max-width: 1400px; | ||||
|     margin: 0 auto; | ||||
|     padding-top: 60px; | ||||
|     padding-bottom: 20px;; | ||||
| } | ||||
|  | ||||
| .tray { | ||||
|     background: #ffffff; | ||||
|     padding: 15px; | ||||
|     margin: 10px 0; | ||||
|     border-radius: 8px; | ||||
|     box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | ||||
|     border-left: 4px solid #ffffff; | ||||
| } | ||||
|  | ||||
| .tray p { | ||||
|     margin: 5px 0; | ||||
| } | ||||
|  | ||||
| .tray b { | ||||
|     color: #007bff; | ||||
| } | ||||
|  | ||||
| /* Responsive Design */ | ||||
| @media (max-width: 1024px) { | ||||
|     .three-column-layout { | ||||
|         flex-direction: column; | ||||
|     } | ||||
|      | ||||
|     .column { | ||||
|         width: 100%; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .nfc-status-display { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 10px; | ||||
|     padding: 10px 0; | ||||
| } | ||||
|  | ||||
| .status-circle { | ||||
|     width: 20px; | ||||
|     height: 20px; | ||||
|     border-radius: 50%; | ||||
|     display: inline-block; | ||||
|     border: 2px solid #ccc; | ||||
|     background-color: #ffffff; | ||||
| } | ||||
|  | ||||
| .status-circle.success { | ||||
|     background-color: #28a745; | ||||
|     border-color: #218838; | ||||
| } | ||||
|  | ||||
| .status-circle.error { | ||||
|     background-color: #dc3545; | ||||
|     border-color: #c82333; | ||||
| } | ||||
|  | ||||
| .nfc-data { | ||||
|     padding: 10px; | ||||
|     background-color: #f8f9fa; | ||||
|     border-radius: 4px; | ||||
|     margin-top: 5px; | ||||
|     width: 100%; | ||||
| } | ||||
|  | ||||
| .nfc-data p { | ||||
|     margin: 5px 0; | ||||
|     font-size: 0.9em; | ||||
| } | ||||
|  | ||||
| .nfc-status-display { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     gap: 10px; | ||||
| } | ||||
|  | ||||
| .error-message { | ||||
|     padding: 10px; | ||||
|     background-color: #fff3f3; | ||||
|     border-radius: 4px; | ||||
|     border-left: 4px solid #dc3545; | ||||
| } | ||||
|  | ||||
| .info-message { | ||||
|     padding: 10px; | ||||
|     background-color: #fff3f3; | ||||
|     border-radius: 4px; | ||||
|     border-left: 4px solid #39d82e; | ||||
| } | ||||
|  | ||||
| .nfc-header { | ||||
|     display: grid; | ||||
|     grid-template-columns: 40px 1fr 40px; | ||||
|     align-items: center; | ||||
|     margin-bottom: 10px; | ||||
| } | ||||
|  | ||||
| .nfc-header h2 { | ||||
|     margin: 0; | ||||
|     grid-column: 2; | ||||
|     text-align: center; | ||||
| } | ||||
|  | ||||
| .nfc-header .status-circle { | ||||
|     grid-column: 3; | ||||
|     justify-self: end; | ||||
| } | ||||
|  | ||||
| .content-header { | ||||
|     display: flex; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     position: relative; | ||||
| } | ||||
|  | ||||
| .connection-status { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 10px; | ||||
|     background-color: #fff3f3; | ||||
|     border: 1px solid #dc3545; | ||||
|     border-radius: 4px; | ||||
|     padding: 10px 15px; | ||||
|     margin: 15px auto; | ||||
|     color: #dc3545; | ||||
|     position: fixed; | ||||
|     top: 20px; | ||||
|     left: 50%; | ||||
|     transform: translateX(-50%); | ||||
|     z-index: 1000; | ||||
|     box-shadow: 0 2px 4px rgba(0,0,0,0.1); | ||||
|     max-width: 90%; | ||||
|     opacity: 0; | ||||
|     transition: opacity 0.3s ease; | ||||
| } | ||||
|  | ||||
| .connection-status.visible { | ||||
|     opacity: 1; | ||||
| } | ||||
|  | ||||
| .spinner { | ||||
|     flex-shrink: 0; | ||||
|     width: 16px; | ||||
|     height: 16px; | ||||
|     border: 2px solid rgba(220, 53, 69, 0.2); | ||||
|     border-top-color: #dc3545; | ||||
|     border-radius: 50%; | ||||
| } | ||||
|  | ||||
| .connection-status.visible .spinner { | ||||
|     animation: spin 0.8s linear infinite; | ||||
| } | ||||
|  | ||||
| @keyframes spin { | ||||
|     to { transform: rotate(360deg); } | ||||
| } | ||||
|  | ||||
| .connection-status.hidden { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| .nfc-actions { | ||||
|     display: flex; | ||||
|     gap: 10px; | ||||
|     justify-content: center; /* Zentriert das div */ | ||||
| } | ||||
|  | ||||
| .btn { | ||||
|     padding: 8px 16px; | ||||
|     border-radius: 4px; | ||||
|     border: none; | ||||
|     cursor: pointer; | ||||
|     font-weight: bold; | ||||
| } | ||||
|  | ||||
| .btn-primary { | ||||
|     background-color: #007bff; | ||||
|     color: white; | ||||
| } | ||||
|  | ||||
| .btn-primary:hover { | ||||
|     background-color: #0056b3; | ||||
| } | ||||
|  | ||||
| .btn-danger { | ||||
|     background-color: #dc3545; | ||||
|     color: white; | ||||
| } | ||||
|  | ||||
| .btn-danger:hover { | ||||
|     background-color: #c82333; | ||||
| } | ||||
|  | ||||
| /* Filament Select Styling */ | ||||
| #filamentSelect { | ||||
|     width: 100%; | ||||
|     padding: 12px 15px; | ||||
|     font-size: 16px; | ||||
|     border: 2px solid #e0e0e0; | ||||
|     border-radius: 8px; | ||||
|     background-color: #fff; | ||||
|     cursor: pointer; | ||||
|     appearance: none; | ||||
|     -webkit-appearance: none; | ||||
|     -moz-appearance: none; | ||||
|     background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23007bff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e"); | ||||
|     background-repeat: no-repeat; | ||||
|     background-position: right 15px center; | ||||
|     background-size: 15px; | ||||
|     transition: all 0.3s ease; | ||||
|     box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | ||||
| } | ||||
|  | ||||
| #filamentSelect:hover { | ||||
|     border-color: #007bff; | ||||
|     box-shadow: 0 3px 6px rgba(0, 123, 255, 0.1); | ||||
| } | ||||
|  | ||||
| #filamentSelect:focus { | ||||
|     border-color: #007bff; | ||||
|     outline: none; | ||||
|     box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25); | ||||
| } | ||||
|  | ||||
| #filamentSelect option { | ||||
|     padding: 8px 15px; | ||||
|     font-size: 16px; | ||||
|     background-color: #fff; | ||||
|     color: #000; /* Standard Textfarbe für alles außer dem Farbblock */ | ||||
| } | ||||
|  | ||||
| #filamentSelect option::first-letter { | ||||
|     font-size: 16px; | ||||
|     margin-right: 5px; | ||||
| } | ||||
|  | ||||
| #filamentSelect option::before { | ||||
|     content: ''; | ||||
|     display: inline-block; | ||||
|     width: 12px; | ||||
|     margin-right: 8px; | ||||
| } | ||||
|  | ||||
| /* Color Box im Select */ | ||||
| .color-box { | ||||
|     display: inline-block; | ||||
|     width: 12px; | ||||
|     height: 12px; | ||||
|     border: 1px solid #333; | ||||
|     border-radius: 2px; | ||||
|     margin-right: 5px; | ||||
|     vertical-align: middle; | ||||
| } | ||||
|  | ||||
| #filamentSelect option span { | ||||
|     display: inline-block; | ||||
|     pointer-events: none; | ||||
| } | ||||
|  | ||||
| #filamentSelect option span:first-child { | ||||
|     margin-right: 5px; | ||||
|     font-size: 16px; | ||||
| } | ||||
|  | ||||
| /* Filament Select Option Styling */ | ||||
| #filamentSelect option span.color-circle { | ||||
|     display: inline-block; | ||||
|     width: 12px; | ||||
|     height: 12px; | ||||
|     border-radius: 50%; | ||||
|     margin-right: 8px; | ||||
|     border: 1px solid #333; | ||||
|     vertical-align: middle; | ||||
| } | ||||
|  | ||||
| /* Custom Dropdown */ | ||||
| .custom-dropdown { | ||||
|     position: relative; | ||||
|     width: 100%; | ||||
|     font-family: inherit; | ||||
|     cursor: default; /* Container selbst soll normalen Cursor haben */ | ||||
| } | ||||
|  | ||||
| .dropdown-button { | ||||
|     padding: 12px 15px; | ||||
|     border: 2px solid #e0e0e0; | ||||
|     border-radius: 8px; | ||||
|     background-color: #fff; | ||||
|     cursor: pointer; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 8px; | ||||
|     transition: all 0.3s ease; | ||||
| } | ||||
|  | ||||
| .dropdown-button:hover { | ||||
|     border-color: #007bff; | ||||
| } | ||||
|  | ||||
| .selected-color { | ||||
|     width: 16px; | ||||
|     height: 16px; | ||||
|     border-radius: 50%; | ||||
|     border: 1px solid #333; | ||||
|     flex-shrink: 0; | ||||
| } | ||||
|  | ||||
| .dropdown-arrow { | ||||
|     margin-left: auto; | ||||
|     color: #007bff; | ||||
|     font-size: 12px; | ||||
| } | ||||
|  | ||||
| .dropdown-content { | ||||
|     display: none; | ||||
|     position: absolute; | ||||
|     top: 100%; | ||||
|     left: 0; | ||||
|     right: 0; | ||||
|     background-color: #fff; | ||||
|     border: 1px solid #e0e0e0; | ||||
|     border-radius: 8px; | ||||
|     margin-top: 4px; | ||||
|     max-height: 300px; | ||||
|     overflow-y: auto; | ||||
|     z-index: 1000; | ||||
|     box-shadow: 0 2px 4px rgba(0,0,0,0.1); | ||||
| } | ||||
|  | ||||
| .dropdown-content.show { | ||||
|     display: block; | ||||
| } | ||||
|  | ||||
| .dropdown-option { | ||||
|     padding: 10px 15px; | ||||
|     cursor: pointer; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 8px; | ||||
| } | ||||
|  | ||||
| .dropdown-option:hover { | ||||
|     background-color: #f8f9fa; | ||||
| } | ||||
|  | ||||
| .option-color { | ||||
|     width: 16px; | ||||
|     height: 16px; | ||||
|     border-radius: 50%; | ||||
|     border: 1px solid #333; | ||||
|     flex-shrink: 0; | ||||
| } | ||||
|  | ||||
| .notification { | ||||
|     position: fixed; | ||||
|     top: 20px; | ||||
|     right: 20px; | ||||
|     padding: 15px 25px; | ||||
|     border-radius: 4px; | ||||
|     color: white; | ||||
|     z-index: 1000; | ||||
|     animation: slideIn 0.3s ease-out; | ||||
| } | ||||
|  | ||||
| .notification.success { | ||||
|     background-color: #28a745; | ||||
| } | ||||
|  | ||||
| .notification.error { | ||||
|     background-color: #dc3545; | ||||
| } | ||||
|  | ||||
| .notification.fade-out { | ||||
|     opacity: 0; | ||||
|     transition: opacity 0.3s ease-out; | ||||
| } | ||||
|  | ||||
| @keyframes slideIn { | ||||
|     from { | ||||
|         transform: translateX(100%); | ||||
|         opacity: 0; | ||||
|     } | ||||
|     to { | ||||
|         transform: translateX(0); | ||||
|         opacity: 1; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /* Neue Styles für die Statistiken */ | ||||
| .statistics-grid { | ||||
|     display: grid; | ||||
|     grid-template-columns: 1fr 1fr; | ||||
|     gap: 20px; | ||||
|     margin-top: 15px; | ||||
| } | ||||
|  | ||||
| .statistics-column { | ||||
|     background: #f8f9fa; | ||||
|     padding: 0; | ||||
|     border-radius: 8px; | ||||
|     box-shadow: 0 2px 4px rgba(0,0,0,0.05); | ||||
| } | ||||
|  | ||||
| .statistics-column h3 { | ||||
|     color: #007bff; | ||||
|     margin-bottom: 5px; | ||||
|     padding-bottom: 8px; | ||||
|     border-bottom: 2px solid #e9ecef; | ||||
|     font-size: 1.1rem; | ||||
| } | ||||
|  | ||||
| .statistics-list { | ||||
|     list-style: none; | ||||
|     padding: 0; | ||||
|     margin: 0; | ||||
| } | ||||
|  | ||||
| .statistics-list li { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     padding: 8px 5px 0 5px; | ||||
|     border-bottom: 1px solid #e9ecef; | ||||
| } | ||||
|  | ||||
| .statistics-list li:last-child { | ||||
|     border-bottom: none; | ||||
| } | ||||
|  | ||||
| .stat-label { | ||||
|     color: #495057; | ||||
|     font-weight: 500; | ||||
| } | ||||
|  | ||||
| .stat-value { | ||||
|     font-weight: bold; | ||||
|     color: #007bff; | ||||
| } | ||||
|  | ||||
| /* Responsive Design Anpassung */ | ||||
| @media (max-width: 768px) { | ||||
|     .statistics-grid { | ||||
|         grid-template-columns: 1fr; | ||||
|     } | ||||
| } | ||||
|  | ||||
| .statistics-header { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
|     margin-bottom: 15px; | ||||
|     padding-bottom: 0; | ||||
|     border-bottom: 1px solid #e9ecef; | ||||
| } | ||||
|  | ||||
| .refresh-button { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     padding: 8px 16px; | ||||
|     background-color: #007bff; | ||||
|     color: white; | ||||
|     border: none; | ||||
|     border-radius: 4px; | ||||
|     cursor: pointer; | ||||
|     font-size: 14px; | ||||
|     transition: background-color 0.2s; | ||||
| } | ||||
|  | ||||
| .refresh-button:hover { | ||||
|     background-color: #0056b3; | ||||
| } | ||||
|  | ||||
| .refresh-button:active { | ||||
|     background-color: #004494; | ||||
| } | ||||
|  | ||||
| .spools-info { | ||||
|     display: flex; | ||||
|     justify-content: flex-start; | ||||
|     gap: 20px; | ||||
|     margin-bottom: 15px; | ||||
| } | ||||
|  | ||||
| .spool-stat { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 8px; | ||||
| } | ||||
|  | ||||
| .spool-stat .stat-label { | ||||
|     color: #495057; | ||||
|     font-weight: 500; | ||||
|     white-space: nowrap; | ||||
| } | ||||
|  | ||||
| .spool-stat .stat-value { | ||||
|     font-weight: bold; | ||||
|     color: #007bff; | ||||
| } | ||||
|  | ||||
| /* Buttons und klickbare Elemente */ | ||||
| button,  | ||||
| input[type="submit"], | ||||
| .dropdown-button, | ||||
| .dropdown-option, | ||||
| .refresh-button, | ||||
| .btn, | ||||
| .styled-select, | ||||
| select, | ||||
| a { | ||||
|     cursor: pointer !important; | ||||
| } | ||||
|  | ||||
| /* Disabled Zustände */ | ||||
| button:disabled, | ||||
| input[type="submit"]:disabled, | ||||
| .btn:disabled, | ||||
| .styled-select:disabled { | ||||
|     cursor: not-allowed !important; | ||||
|     opacity: 0.7; | ||||
| } | ||||
|  | ||||
| /* Schreib-Button */ | ||||
| #writeNfcButton { | ||||
|     background-color: #007bff; | ||||
|     color: white; | ||||
|     transition: background-color 0.3s, color 0.3s; | ||||
|     width: 160px; | ||||
| } | ||||
|  | ||||
| #writeNfcButton.writing { | ||||
|     background-color: #ffc107; | ||||
|     color: black; | ||||
|     width: 160px; | ||||
| } | ||||
|  | ||||
| #writeNfcButton.success { | ||||
|     background-color: #28a745; | ||||
|     color: white; | ||||
|     width: 160px; | ||||
| } | ||||
|  | ||||
| #writeNfcButton.error { | ||||
|     background-color: #dc3545; | ||||
|     color: white; | ||||
|     width: 160px; | ||||
| } | ||||
|  | ||||
| @keyframes dots { | ||||
|     0% { content: ""; } | ||||
|     33% { content: "."; } | ||||
|     66% { content: ".."; } | ||||
|     100% { content: "..."; } | ||||
| } | ||||
|  | ||||
| #writeNfcButton.writing::after { | ||||
|     content: "..."; | ||||
|     animation: dots 1s steps(3, end) infinite; | ||||
| } | ||||
|  | ||||
| .reboot-button { | ||||
|     background-color: #ff0000; | ||||
|     color: white; | ||||
|     padding: 10px 20px; | ||||
|     border: none; | ||||
|     border-radius: 4px; | ||||
|     margin-left: 10px; | ||||
|     cursor: pointer; | ||||
| } | ||||
|  | ||||
| .reboot-button:hover { | ||||
|     background-color: #cc0000; | ||||
| } | ||||
|  | ||||
| /* Bambu Settings Erweiterung */ | ||||
| .bambu-settings { | ||||
|     background: white; | ||||
|     padding: 20px; | ||||
|     border-radius: 8px; | ||||
|     box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | ||||
|     max-width: 400px; | ||||
|     margin: 20px auto; | ||||
| } | ||||
|  | ||||
| .bambu-settings .input-group { | ||||
|     margin-bottom: 15px; | ||||
|     text-align: left; | ||||
| } | ||||
|  | ||||
| .bambu-settings .input-group label { | ||||
|     display: block; | ||||
|     margin-bottom: 5px; | ||||
| } | ||||
|  | ||||
| .bambu-settings .input-group input { | ||||
|     width: 100%; | ||||
| } | ||||
|  | ||||
| #bambuStatusMessage { | ||||
|     margin-top: 15px; | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 10px; | ||||
|     justify-content: center; | ||||
| } | ||||
|  | ||||
|  | ||||
| .tray { | ||||
|     position: relative; | ||||
| } | ||||
|  | ||||
| .spool-button:hover { | ||||
|     opacity: 0.8; | ||||
| } | ||||
							
								
								
									
										99
									
								
								html/waage.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								html/waage.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| {{header}} | ||||
|     <div class="content"> | ||||
|         <h1>Scale Configuration Page</h1> | ||||
|          | ||||
|         <div class="card"> | ||||
|             <div class="card-body"> | ||||
|                 <h5 class="card-title">Sacle Calibration</h5> | ||||
|                 <button id="calibrateBtn" class="btn btn-primary">Calibrate Scale</button> | ||||
|                 <button id="tareBtn" class="btn btn-secondary">Tare Scale</button> | ||||
|                 <div id="statusMessage" class="mt-3"></div> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <!-- Neue Kalibrierungskarte --> | ||||
|         <div id="calibrationCard" class="card mt-3" style="display: none;"> | ||||
|             <div class="card-body"> | ||||
|                 <h5 class="card-title">Calibration done</h5> | ||||
|                 <p>Please follow these steps:</p> | ||||
|                 <ol> | ||||
|                     <li>Make sure the scale is empty</li> | ||||
|                     <li>Have a 500g calibration weight ready</li> | ||||
|                     <li>Click on "Start Calibration"</li> | ||||
|                     <li>Follow the further instructions</li> | ||||
|                 </ol> | ||||
|                 <ol> | ||||
|                     <li>Step 1: Empty the scale</li> | ||||
|                     <li>Step 2: Place the 500g weight on the scale</li> | ||||
|                     <li>Step 3: Remove weight from Scale</li> | ||||
|                 </ol> | ||||
|                 <button id="startCalibrationBtn" class="btn btn-danger">Start Calibration</button> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <script> | ||||
|         let ws = null; | ||||
|         const statusMessage = document.getElementById('statusMessage'); | ||||
|  | ||||
|         function connectWebSocket() { | ||||
|             ws = new WebSocket(`ws://${window.location.hostname}/ws`); | ||||
|              | ||||
|             ws.onopen = () => { | ||||
|                 console.log('WebSocket verbunden'); | ||||
|                 statusMessage.innerHTML = 'Scale connected'; | ||||
|                 enableButtons(true); | ||||
|             }; | ||||
|  | ||||
|             ws.onclose = () => { | ||||
|                 console.log('WebSocket getrennt'); | ||||
|                 statusMessage.innerHTML = 'Scale connection lost'; | ||||
|                 enableButtons(false); | ||||
|                 setTimeout(connectWebSocket, 2000); | ||||
|             }; | ||||
|  | ||||
|             ws.onmessage = (event) => { | ||||
|                 const data = JSON.parse(event.data); | ||||
|                 if (data.type === 'scale') { | ||||
|                     if (data.payload === 'success') { | ||||
|                         statusMessage.innerHTML = 'Well done'; | ||||
|                         statusMessage.className = 'alert alert-success'; | ||||
|                     } else if (data.payload === 'error') { | ||||
|                         statusMessage.innerHTML = 'Error while action'; | ||||
|                         statusMessage.className = 'alert alert-danger'; | ||||
|                     } | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         function enableButtons(enabled) { | ||||
|             document.getElementById('calibrateBtn').disabled = !enabled; | ||||
|             document.getElementById('tareBtn').disabled = !enabled; | ||||
|         } | ||||
|  | ||||
|         document.getElementById('calibrateBtn').addEventListener('click', () => { | ||||
|             // Kalibrierungskarte anzeigen | ||||
|             document.getElementById('calibrationCard').style.display = 'block'; | ||||
|         }); | ||||
|  | ||||
|         document.getElementById('startCalibrationBtn').addEventListener('click', () => { | ||||
|             ws.send(JSON.stringify({ | ||||
|                 type: 'scale', | ||||
|                 payload: 'calibrate' | ||||
|             })); | ||||
|             // Optional: Kalibrierungskarte nach dem Start ausblenden | ||||
|             document.getElementById('calibrationCard').style.display = 'none'; | ||||
|         }); | ||||
|  | ||||
|         document.getElementById('tareBtn').addEventListener('click', () => { | ||||
|             ws.send(JSON.stringify({ | ||||
|                 type: 'scale', | ||||
|                 payload: 'tare' | ||||
|             })); | ||||
|         }); | ||||
|  | ||||
|         // WebSocket-Verbindung beim Laden der Seite initiieren | ||||
|         connectWebSocket(); | ||||
|     </script> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										12
									
								
								html/wifi.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								html/wifi.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| {{header}} | ||||
|     <div class="content"> | ||||
|         <h1>WiFi Configuration Page</h1> | ||||
|         <form action="/setToken" method="post"> | ||||
|             <label for="deviceToken">Device Token:</label><br> | ||||
|             <input type="text" id="deviceToken" name="deviceToken"><br> | ||||
|             <input type="submit" value="Set Token"> | ||||
|         </form> | ||||
|         <p>Configure your WiFi settings here.</p> | ||||
|     </div> | ||||
| </body> | ||||
| </html> | ||||
		Reference in New Issue
	
	Block a user