const RaspberryDisplay = { name: 'RaspberryDisplay', template: `

NOC Display Verwaltung

Keine Displays konfiguriert

Fügen Sie Ihr erstes Display hinzu, um zu beginnen

{{ groupName }} {{ displays.length }}
{{ display.monitor_size }}"
{{ display.display_label }}
HDMI:{{ display.hdmi_port }} {{ display.ip_address }}
{{ displayStatuses[display.ip_address]?.temperature_c?.toFixed(1) || '--' }}°C {{ displayStatuses[display.ip_address]?.cpu_percent?.toFixed(0) || '--' }}%
Displays hierher ziehen
Gruppe hinzufügen

Raspberry Pi suchen

{{ discoveredPi.hostname || 'Raspberry Pi' }} gefunden
HDMI:{{ disp.hdmi_port }} {{ truncateUrl(disp.current_url) || 'Keine URL' }}
{{ discoverError }}

Möchten Sie {{ deleteTarget?.display_label }} wirklich löschen?

Diese Aktion kann nicht rückgängig gemacht werden.

Displays werden aktualisiert
{{ refreshProgress.current }} / {{ refreshProgress.total }} - {{ refreshProgress.currentDisplay }}
`, data() { return { // Data groupedDisplays: {}, displayStatuses: {}, availableGroups: [], // UI State loading: true, searchQuery: '', collapsedGroups: {}, // Modals showDisplayModal: false, showAddGroupModal: false, showDeleteModal: false, editingDisplay: null, deleteTarget: null, // Form Data formData: this.getEmptyFormData(), newGroupName: '', newGroupNameInput: '', saving: false, deleting: false, // Discovery discoverIp: '', discoverPort: 5000, discovering: false, discoveredPi: null, discoverError: '', // URL Editing editingUrlId: null, editUrlValue: '', // Drag and Drop draggingDisplay: null, dragOverGroup: null, // Bulk Operations refreshingAll: false, refreshProgress: { current: 0, total: 0, currentDisplay: '' }, poweringAll: false, rebootingAll: false, refreshingDisplays: {}, // Polling statusPollInterval: null }; }, computed: { filteredGroups() { if (!this.searchQuery.trim()) { return this.groupedDisplays; } const query = this.searchQuery.toLowerCase(); const filtered = {}; for (const [groupName, displays] of Object.entries(this.groupedDisplays)) { const matchingDisplays = displays.filter(d => d.display_label.toLowerCase().includes(query) || d.ip_address.includes(query) || d.hostname?.toLowerCase().includes(query) || d.display_url?.toLowerCase().includes(query) ); if (matchingDisplays.length > 0) { filtered[groupName] = matchingDisplays; } } return filtered; } }, methods: { getEmptyFormData() { return { display_label: '', hostname: '', ip_address: '', display_url: '', group_name: '', monitor_size: '27', hdmi_port: 0, agent_port: 5000, custom_style: '' }; }, async loadDisplays() { try { const response = await axios.get(window.TT_CONFIG.BASE_URL + '/api', { params: { do: 'getDisplays' } }); if (response.data.status === 'OK') { this.groupedDisplays = response.data.result; this.availableGroups = Object.keys(this.groupedDisplays); } } catch (error) { console.error('Failed to load displays:', error); } finally { this.loading = false; } }, async loadStatuses() { try { const response = await axios.get(window.TT_CONFIG.BASE_URL + '/api', { params: { do: 'getBatchStatus' } }); if (response.data.status === 'OK') { this.displayStatuses = response.data.result; } } catch (error) { console.error('Failed to load statuses:', error); } }, startStatusPolling() { this.loadStatuses(); this.statusPollInterval = setInterval(() => { this.loadStatuses(); }, 30000); }, stopStatusPolling() { if (this.statusPollInterval) { clearInterval(this.statusPollInterval); } }, // Display Status getStatusClass(display) { const status = this.displayStatuses[display.ip_address]; return status?.online ? 'online' : 'offline'; }, getTempClass(status) { if (!status?.temperature_c) return ''; if (status.temperature_c >= 80) return 'critical'; if (status.temperature_c >= 70) return 'warn'; return ''; }, truncateUrl(url) { if (!url) return ''; const cleanUrl = url.replace(/^https?:\/\/(www\.)?/, ''); return cleanUrl.length > 30 ? cleanUrl.substring(0, 30) + '...' : cleanUrl; }, // Group Management toggleGroup(groupName) { this.collapsedGroups[groupName] = !this.collapsedGroups[groupName]; }, openAddGroupModal() { this.newGroupNameInput = ''; this.showAddGroupModal = true; }, async createGroup() { if (!this.newGroupNameInput.trim()) return; this.availableGroups.push(this.newGroupNameInput.trim()); if (!this.groupedDisplays[this.newGroupNameInput.trim()]) { this.groupedDisplays[this.newGroupNameInput.trim()] = []; } this.showAddGroupModal = false; }, // Display CRUD openAddModal() { this.editingDisplay = null; this.formData = this.getEmptyFormData(); this.discoveredPi = null; this.discoverError = ''; this.discoverIp = ''; this.newGroupName = ''; this.showDisplayModal = true; }, openEditModal(display) { this.editingDisplay = display; this.formData = { ...display }; this.showDisplayModal = true; }, closeDisplayModal() { this.showDisplayModal = false; this.editingDisplay = null; this.discoveredPi = null; }, async saveDisplay() { let groupName = this.formData.group_name; if (groupName === '__new__') { groupName = this.newGroupName.trim(); if (!groupName) { alert('Bitte geben Sie einen Gruppennamen ein'); return; } } if (!this.formData.display_label || !this.formData.ip_address || !groupName) { alert('Bitte füllen Sie alle Pflichtfelder aus'); return; } this.saving = true; try { const action = this.editingDisplay ? 'updateDisplay' : 'createDisplay'; const params = { do: action, ...this.formData, group_name: groupName }; if (this.editingDisplay) { params.id = this.editingDisplay.id; } const response = await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, { params }); if (response.data.status === 'OK' || response.data.status === 'success') { this.closeDisplayModal(); await this.loadDisplays(); } else { alert('Speichern fehlgeschlagen'); } } catch (error) { console.error('Failed to save display:', error); alert('Speichern fehlgeschlagen'); } finally { this.saving = false; } }, confirmDelete(display) { this.deleteTarget = display; this.showDeleteModal = true; }, async deleteDisplay() { if (!this.deleteTarget) return; this.deleting = true; try { const response = await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, { params: { do: 'deleteDisplay', id: this.deleteTarget.id } }); if (response.data.status === 'OK' || response.data.status === 'success') { this.showDeleteModal = false; this.deleteTarget = null; await this.loadDisplays(); } } catch (error) { console.error('Failed to delete display:', error); } finally { this.deleting = false; } }, // Discovery async discoverPi() { if (!this.discoverIp) return; this.discovering = true; this.discoveredPi = null; this.discoverError = ''; try { const response = await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, { params: { do: 'discoverPi', ip_address: this.discoverIp, agent_port: this.discoverPort } }); if (response.data.status === 'OK' && response.data.result.online) { this.discoveredPi = response.data.result; this.formData.ip_address = this.discoverIp; this.formData.agent_port = this.discoverPort; this.formData.hostname = this.discoveredPi.hostname || ''; } else { this.discoverError = response.data.result?.error || 'Verbindung zum Raspberry Pi fehlgeschlagen'; } } catch (error) { this.discoverError = 'Suche fehlgeschlagen'; } finally { this.discovering = false; } }, selectDiscoveredDisplay(disp, idx) { this.formData.hdmi_port = disp.hdmi_port; this.formData.display_url = disp.current_url || ''; this.formData.display_label = `${this.discoveredPi.hostname || 'Pi'} HDMI:${disp.hdmi_port}`; }, // URL Editing startEditUrl(display) { this.editingUrlId = display.id; this.editUrlValue = display.display_url || ''; this.$nextTick(() => { const input = this.$refs.urlInput; if (input) { if (Array.isArray(input)) { input[0]?.focus(); } else { input.focus(); } } }); }, cancelEditUrl() { this.editingUrlId = null; this.editUrlValue = ''; }, async saveUrl(display) { if (this.editUrlValue === display.display_url) { this.cancelEditUrl(); return; } try { const response = await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, { params: { do: 'setUrl', id: display.id, url: this.editUrlValue } }); if (response.data.status === 'OK' || response.data.result?.success) { display.display_url = this.editUrlValue; } } catch (error) { console.error('Failed to set URL:', error); } finally { this.cancelEditUrl(); } }, // Display Actions async refreshDisplay(display) { this.refreshingDisplays[display.id] = true; try { await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, { params: { do: 'refreshDisplay', id: display.id } }); } catch (error) { console.error('Failed to refresh display:', error); } finally { this.refreshingDisplays[display.id] = false; } }, async togglePower(display) { const status = this.displayStatuses[display.ip_address]; const state = status?.displays?.[display.hdmi_port]?.cec_state === 'on' ? 'off' : 'on'; try { await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, { params: { do: 'cecPower', id: display.id, state } }); await this.loadStatuses(); } catch (error) { console.error('Failed to toggle power:', error); } }, // Bulk Operations async refreshAllDisplays() { const allDisplays = Object.values(this.groupedDisplays).flat(); if (allDisplays.length === 0) return; this.refreshingAll = true; this.refreshProgress = { current: 0, total: allDisplays.length, currentDisplay: '' }; for (const display of allDisplays) { this.refreshProgress.currentDisplay = display.display_label; this.refreshingDisplays[display.id] = true; try { await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, { params: { do: 'refreshDisplay', id: display.id } }); await new Promise(resolve => setTimeout(resolve, 2000)); } catch (error) { console.error('Failed to refresh:', display.display_label); } this.refreshingDisplays[display.id] = false; this.refreshProgress.current++; } this.refreshingAll = false; }, async refreshGroup(groupName) { const displays = this.groupedDisplays[groupName] || []; for (const display of displays) { await this.refreshDisplay(display); await new Promise(resolve => setTimeout(resolve, 1000)); } }, async powerAllDisplays(state) { this.poweringAll = true; try { await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, { params: { do: 'powerAll', state } }); await this.loadStatuses(); } catch (error) { console.error('Failed to power all:', error); } finally { this.poweringAll = false; } }, async rebootAllPis() { if (!confirm('Möchten Sie wirklich alle Raspberry Pis neustarten?')) return; this.rebootingAll = true; try { await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, { params: { do: 'rebootAll' } }); } catch (error) { console.error('Failed to reboot all:', error); } finally { this.rebootingAll = false; } }, // Drag and Drop handleDragStart(event, display) { this.draggingDisplay = display; event.dataTransfer.effectAllowed = 'move'; }, handleDragEnd() { this.draggingDisplay = null; this.dragOverGroup = null; }, handleDragOver(event, groupName) { this.dragOverGroup = groupName; }, handleDragLeave() { this.dragOverGroup = null; }, async handleDrop(event, targetGroup) { if (!this.draggingDisplay) return; const display = this.draggingDisplay; const sourceGroup = display.group_name; if (sourceGroup === targetGroup) { this.dragOverGroup = null; return; } // Update locally first for responsiveness const sourceDisplays = this.groupedDisplays[sourceGroup]; const idx = sourceDisplays.findIndex(d => d.id === display.id); if (idx !== -1) { sourceDisplays.splice(idx, 1); } if (!this.groupedDisplays[targetGroup]) { this.groupedDisplays[targetGroup] = []; } display.group_name = targetGroup; this.groupedDisplays[targetGroup].push(display); // Persist to server try { await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, { params: { do: 'updateDisplay', id: display.id, group_name: targetGroup } }); } catch (error) { console.error('Failed to update display group:', error); await this.loadDisplays(); } this.dragOverGroup = null; } }, mounted() { this.loadDisplays(); this.startStatusPolling(); }, beforeUnmount() { this.stopStatusPolling(); } }; // Register component if (window.VueApp) { window.VueApp.component('raspberry-display', RaspberryDisplay); } else { Vue.component('raspberry-display', RaspberryDisplay); }