raspberrydisplay v2 init
This commit is contained in:
@@ -70,6 +70,9 @@ class RaspberryDisplayController extends mfBaseController {
|
||||
}
|
||||
|
||||
protected function createDisplayApi(): array|bool {
|
||||
$monitorSize = $this->request->monitor_size ?? '27';
|
||||
if (!in_array($monitorSize, ['27', '42', '55', '65'])) $monitorSize = '27';
|
||||
|
||||
$data = [
|
||||
'display_label' => $this->request->display_label ?? '',
|
||||
'hostname' => $this->request->hostname ?? '',
|
||||
@@ -77,7 +80,7 @@ class RaspberryDisplayController extends mfBaseController {
|
||||
'display_url' => $this->request->display_url ?? '',
|
||||
'group_name' => $this->request->group_name ?? '',
|
||||
'group_order' => (int)($this->request->group_order ?? 0),
|
||||
'monitor_size' => $this->request->monitor_size ?? '27',
|
||||
'monitor_size' => $monitorSize,
|
||||
'hdmi_port' => (int)($this->request->hdmi_port ?? 0),
|
||||
'agent_port' => (int)($this->request->agent_port ?? 5000),
|
||||
'custom_style' => $this->request->custom_style ?? null,
|
||||
@@ -234,10 +237,10 @@ class RaspberryDisplayController extends mfBaseController {
|
||||
"BASE_URL" => self::getUrl("RaspberryDisplay"),
|
||||
"DASHBOARD_URL" => self::getUrl("Dashboard"),
|
||||
"MFAPPNAME" => MFAPPNAME_SLUG,
|
||||
"PAGE_TITLE" => "NOC Display Manager",
|
||||
"PAGE_TITLE" => "NOC Display Verwaltung",
|
||||
"PATH" => [
|
||||
["text" => MFAPPNAME_SLUG, "href" => self::getUrl("Dashboard")],
|
||||
["text" => "NOC Display Manager", "href" => self::getUrl("RaspberryDisplay")]
|
||||
["text" => "NOC Display Verwaltung", "href" => self::getUrl("RaspberryDisplay")]
|
||||
]
|
||||
]);
|
||||
$this->layout()->set("additionalCSS", ["css/views/RaspberryDisplay.css"]);
|
||||
|
||||
@@ -114,11 +114,10 @@ class RaspberryDisplayModel
|
||||
}
|
||||
}
|
||||
|
||||
// Set defaults
|
||||
if ($model->group_order === null) {
|
||||
$model->group_order = 0;
|
||||
}
|
||||
if ($model->monitor_size === null) {
|
||||
if (!in_array($model->monitor_size, ['27', '42', '55', '65'])) {
|
||||
$model->monitor_size = '27';
|
||||
}
|
||||
if ($model->hdmi_port === null) {
|
||||
@@ -166,15 +165,30 @@ class RaspberryDisplayModel
|
||||
{
|
||||
$db = FronkDB::singleton();
|
||||
|
||||
$data = $model->data;
|
||||
$data = [
|
||||
'display_label' => $model->display_label,
|
||||
'hostname' => $model->hostname,
|
||||
'ip_address' => $model->ip_address,
|
||||
'display_url' => $model->display_url,
|
||||
'group_name' => $model->group_name,
|
||||
'group_order' => (int)$model->group_order,
|
||||
'monitor_size' => in_array($model->monitor_size, ['27', '42', '55', '65']) ? $model->monitor_size : '27',
|
||||
'hdmi_port' => (int)$model->hdmi_port,
|
||||
'agent_port' => (int)$model->agent_port,
|
||||
'custom_style' => $model->custom_style,
|
||||
'create_by' => $model->create_by,
|
||||
'edit_by' => $model->edit_by,
|
||||
];
|
||||
|
||||
$forceStr = ['monitor_size'];
|
||||
|
||||
if ($model->id) {
|
||||
$data['edit'] = time();
|
||||
$db->update("RaspberryDisplay", $data, "id=" . (int)$model->id);
|
||||
$db->update("RaspberryDisplay", $data, "id=" . (int)$model->id, $forceStr);
|
||||
} else {
|
||||
$data['create'] = time();
|
||||
$data['edit'] = time();
|
||||
$model->id = $db->insert("RaspberryDisplay", $data);
|
||||
$model->id = $db->insert("RaspberryDisplay", $data, $forceStr);
|
||||
}
|
||||
|
||||
return $model;
|
||||
|
||||
@@ -11,13 +11,20 @@
|
||||
--noc-drag-shadow: 0 12px 32px rgba(0, 83, 132, 0.2);
|
||||
}
|
||||
|
||||
/* Main Container */
|
||||
.noc-display-manager {
|
||||
padding: 20px;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.noc-main-card {
|
||||
background: var(--tt-card, #fff);
|
||||
border: 1px solid var(--tt-border, #e9ecef);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.noc-header {
|
||||
display: flex;
|
||||
@@ -46,9 +53,8 @@
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
padding: 12px 16px;
|
||||
background: var(--tt-card, #fff);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--tt-border, #e9ecef);
|
||||
background: var(--tt-card-2, #f8f9fa);
|
||||
border-radius: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -628,6 +634,11 @@
|
||||
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.tt-scope .ghost-btn i,
|
||||
.tt-scope .primary-btn i {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tt-scope .ghost-btn:hover {
|
||||
background: var(--tt-card-2, #f8f9fa);
|
||||
border-color: var(--tt-accent, #005384);
|
||||
|
||||
@@ -1,205 +1,190 @@
|
||||
// NOC Display Manager - Vue 3 Component
|
||||
const RaspberryDisplay = {
|
||||
name: 'RaspberryDisplay',
|
||||
template: `
|
||||
<div class="tt-scope noc-display-manager">
|
||||
<!-- Header -->
|
||||
<div class="noc-header">
|
||||
<h1 class="noc-title">NOC Display Manager</h1>
|
||||
<div class="noc-header-actions">
|
||||
<button class="ghost-btn" @click="openAddModal" title="Add Display">
|
||||
<i class="fa-duotone fa-plus"></i>
|
||||
<span>Add Display</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Bar -->
|
||||
<div class="noc-action-bar">
|
||||
<input
|
||||
type="text"
|
||||
class="noc-search-input"
|
||||
v-model="searchQuery"
|
||||
placeholder="Search displays..."
|
||||
/>
|
||||
<div class="noc-bulk-actions">
|
||||
<button class="ghost-btn" @click="refreshAllDisplays" :disabled="refreshingAll" title="Refresh All">
|
||||
<i class="fa-duotone fa-arrows-rotate" :class="{ 'fa-spin': refreshingAll }"></i>
|
||||
<span>Refresh All</span>
|
||||
</button>
|
||||
<button class="ghost-btn" @click="powerAllDisplays('on')" :disabled="poweringAll" title="Power On All">
|
||||
<i class="fa-duotone fa-power-off"></i>
|
||||
<span>Power On</span>
|
||||
</button>
|
||||
<button class="ghost-btn" @click="powerAllDisplays('off')" :disabled="poweringAll" title="Power Off All">
|
||||
<i class="fa-duotone fa-moon"></i>
|
||||
<span>Power Off</span>
|
||||
</button>
|
||||
<button class="ghost-btn danger" @click="rebootAllPis" :disabled="rebootingAll" title="Reboot All">
|
||||
<i class="fa-duotone fa-rotate"></i>
|
||||
<span>Reboot All</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="noc-loading">
|
||||
<tt-skeleton v-for="i in 3" :key="i" width="100%" height="200px" style="margin-bottom: 16px; border-radius: 12px;" />
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="Object.keys(groupedDisplays).length === 0" class="noc-empty-state">
|
||||
<i class="fa-duotone fa-display-slash"></i>
|
||||
<h3>No Displays Configured</h3>
|
||||
<p>Add your first display to get started</p>
|
||||
<button class="primary-btn" @click="openAddModal">
|
||||
<i class="fa-duotone fa-plus"></i>
|
||||
Add Display
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Groups -->
|
||||
<div v-else class="noc-groups">
|
||||
<div
|
||||
v-for="(displays, groupName) in filteredGroups"
|
||||
:key="groupName"
|
||||
class="noc-group"
|
||||
:class="{ 'is-drop-target': dragOverGroup === groupName }"
|
||||
@dragover.prevent="handleDragOver($event, groupName)"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop($event, groupName)"
|
||||
>
|
||||
<!-- Group Header -->
|
||||
<div class="noc-group-header" @click="toggleGroup(groupName)">
|
||||
<div class="noc-group-title">
|
||||
<i class="fa-duotone fa-chevron-right" :class="{ 'rotated': !collapsedGroups[groupName] }"></i>
|
||||
<span>{{ groupName }}</span>
|
||||
<span class="noc-group-count">{{ displays.length }}</span>
|
||||
</div>
|
||||
<div class="noc-group-actions" @click.stop>
|
||||
<button class="ghost-btn small" @click="refreshGroup(groupName)" title="Refresh Group">
|
||||
<i class="fa-duotone fa-arrows-rotate"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Group Body -->
|
||||
<div class="noc-group-body" v-show="!collapsedGroups[groupName]">
|
||||
<div
|
||||
v-for="display in displays"
|
||||
:key="display.id"
|
||||
class="noc-display-card"
|
||||
:class="[
|
||||
'size-' + display.monitor_size,
|
||||
{ 'is-refreshing': refreshingDisplays[display.id] },
|
||||
{ 'is-dragging': draggingDisplay?.id === display.id }
|
||||
]"
|
||||
draggable="true"
|
||||
@dragstart="handleDragStart($event, display)"
|
||||
@dragend="handleDragEnd"
|
||||
>
|
||||
<!-- Drag Handle -->
|
||||
<div class="noc-drag-handle">
|
||||
<i class="fa-solid fa-grip-vertical"></i>
|
||||
</div>
|
||||
|
||||
<!-- Card Header -->
|
||||
<div class="noc-card-header">
|
||||
<div class="noc-status-dot" :class="getStatusClass(display)"></div>
|
||||
<span class="noc-monitor-size">{{ display.monitor_size }}"</span>
|
||||
</div>
|
||||
|
||||
<!-- Card Content -->
|
||||
<div class="noc-card-content">
|
||||
<div class="noc-display-label">{{ display.display_label }}</div>
|
||||
<div class="noc-display-info">
|
||||
<span class="noc-hdmi-badge">HDMI:{{ display.hdmi_port }}</span>
|
||||
<span class="noc-ip-badge" :title="display.ip_address">{{ display.ip_address }}</span>
|
||||
</div>
|
||||
<div class="noc-display-url" @click="startEditUrl(display)" :title="display.display_url">
|
||||
<template v-if="editingUrlId === display.id">
|
||||
<input
|
||||
type="text"
|
||||
v-model="editUrlValue"
|
||||
@keyup.enter="saveUrl(display)"
|
||||
@keyup.escape="cancelEditUrl"
|
||||
@blur="saveUrl(display)"
|
||||
ref="urlInput"
|
||||
class="noc-url-input"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ truncateUrl(display.display_url) || 'Click to set URL' }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metrics -->
|
||||
<div class="noc-metrics" v-if="displayStatuses[display.ip_address]?.online">
|
||||
<span class="noc-metric-chip" :class="getTempClass(displayStatuses[display.ip_address])">
|
||||
<i class="fa-solid fa-temperature-half"></i>
|
||||
{{ displayStatuses[display.ip_address]?.temperature_c?.toFixed(1) || '--' }}°C
|
||||
</span>
|
||||
<span class="noc-metric-chip">
|
||||
<i class="fa-solid fa-microchip"></i>
|
||||
{{ displayStatuses[display.ip_address]?.cpu_percent?.toFixed(0) || '--' }}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="noc-actions">
|
||||
<button class="noc-action-btn" @click="openEditModal(display)" title="Edit">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</button>
|
||||
<button class="noc-action-btn" @click="refreshDisplay(display)" :disabled="refreshingDisplays[display.id]" title="Refresh">
|
||||
<i class="fa-solid fa-arrows-rotate" :class="{ 'fa-spin': refreshingDisplays[display.id] }"></i>
|
||||
</button>
|
||||
<button class="noc-action-btn" @click="togglePower(display)" title="Power">
|
||||
<i class="fa-solid fa-power-off"></i>
|
||||
</button>
|
||||
<button class="noc-action-btn danger" @click="confirmDelete(display)" title="Delete">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty Group -->
|
||||
<div v-if="displays.length === 0" class="noc-group-empty">
|
||||
Drop displays here
|
||||
</div>
|
||||
<div class="noc-main-card">
|
||||
<div class="noc-header">
|
||||
<h1 class="noc-title">NOC Display Verwaltung</h1>
|
||||
<div class="noc-header-actions">
|
||||
<button class="ghost-btn" @click="openAddModal" title="Display hinzufügen">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span>Display hinzufügen</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Group Button -->
|
||||
<div class="noc-add-group" @click="openAddGroupModal">
|
||||
<i class="fa-duotone fa-plus"></i>
|
||||
<span>Add Group</span>
|
||||
<div class="noc-action-bar">
|
||||
<input
|
||||
type="text"
|
||||
class="noc-search-input"
|
||||
v-model="searchQuery"
|
||||
placeholder="Displays suchen..."
|
||||
/>
|
||||
<div class="noc-bulk-actions">
|
||||
<button class="ghost-btn" @click="refreshAllDisplays" :disabled="refreshingAll" title="Alle aktualisieren">
|
||||
<i class="fa-duotone fa-arrows-rotate" :class="{ 'fa-spin': refreshingAll }"></i>
|
||||
<span>Alle aktualisieren</span>
|
||||
</button>
|
||||
<button class="ghost-btn" @click="powerAllDisplays('on')" :disabled="poweringAll" title="Alle einschalten">
|
||||
<i class="fa-duotone fa-power-off"></i>
|
||||
<span>Einschalten</span>
|
||||
</button>
|
||||
<button class="ghost-btn" @click="powerAllDisplays('off')" :disabled="poweringAll" title="Alle ausschalten">
|
||||
<i class="fa-duotone fa-moon"></i>
|
||||
<span>Ausschalten</span>
|
||||
</button>
|
||||
<button class="ghost-btn danger" @click="rebootAllPis" :disabled="rebootingAll" title="Alle neustarten">
|
||||
<i class="fa-duotone fa-rotate"></i>
|
||||
<span>Neustart</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="noc-loading">
|
||||
<tt-skeleton v-for="i in 3" :key="i" width="100%" height="200px" style="margin-bottom: 16px; border-radius: 12px;" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="Object.keys(groupedDisplays).length === 0" class="noc-empty-state">
|
||||
<i class="fa-duotone fa-display-slash"></i>
|
||||
<h3>Keine Displays konfiguriert</h3>
|
||||
<p>Fügen Sie Ihr erstes Display hinzu, um zu beginnen</p>
|
||||
<button class="primary-btn" @click="openAddModal">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
Display hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else class="noc-groups">
|
||||
<div
|
||||
v-for="(displays, groupName) in filteredGroups"
|
||||
:key="groupName"
|
||||
class="noc-group"
|
||||
:class="{ 'is-drop-target': dragOverGroup === groupName }"
|
||||
@dragover.prevent="handleDragOver($event, groupName)"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop($event, groupName)"
|
||||
>
|
||||
<div class="noc-group-header" @click="toggleGroup(groupName)">
|
||||
<div class="noc-group-title">
|
||||
<i class="fa-duotone fa-chevron-right" :class="{ 'rotated': !collapsedGroups[groupName] }"></i>
|
||||
<span>{{ groupName }}</span>
|
||||
<span class="noc-group-count">{{ displays.length }}</span>
|
||||
</div>
|
||||
<div class="noc-group-actions" @click.stop>
|
||||
<button class="ghost-btn small" @click="refreshGroup(groupName)" title="Gruppe aktualisieren">
|
||||
<i class="fa-duotone fa-arrows-rotate"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noc-group-body" v-show="!collapsedGroups[groupName]">
|
||||
<div
|
||||
v-for="display in displays"
|
||||
:key="display.id"
|
||||
class="noc-display-card"
|
||||
:class="[
|
||||
'size-' + display.monitor_size,
|
||||
{ 'is-refreshing': refreshingDisplays[display.id] },
|
||||
{ 'is-dragging': draggingDisplay?.id === display.id }
|
||||
]"
|
||||
draggable="true"
|
||||
@dragstart="handleDragStart($event, display)"
|
||||
@dragend="handleDragEnd"
|
||||
>
|
||||
<div class="noc-drag-handle">
|
||||
<i class="fa-solid fa-grip-vertical"></i>
|
||||
</div>
|
||||
|
||||
<div class="noc-card-header">
|
||||
<div class="noc-status-dot" :class="getStatusClass(display)"></div>
|
||||
<span class="noc-monitor-size">{{ display.monitor_size }}"</span>
|
||||
</div>
|
||||
|
||||
<div class="noc-card-content">
|
||||
<div class="noc-display-label">{{ display.display_label }}</div>
|
||||
<div class="noc-display-info">
|
||||
<span class="noc-hdmi-badge">HDMI:{{ display.hdmi_port }}</span>
|
||||
<span class="noc-ip-badge" :title="display.ip_address">{{ display.ip_address }}</span>
|
||||
</div>
|
||||
<div class="noc-display-url" @click="startEditUrl(display)" :title="display.display_url">
|
||||
<template v-if="editingUrlId === display.id">
|
||||
<input
|
||||
type="text"
|
||||
v-model="editUrlValue"
|
||||
@keyup.enter="saveUrl(display)"
|
||||
@keyup.escape="cancelEditUrl"
|
||||
@blur="saveUrl(display)"
|
||||
ref="urlInput"
|
||||
class="noc-url-input"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ truncateUrl(display.display_url) || 'Klicken um URL zu setzen' }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noc-metrics" v-if="displayStatuses[display.ip_address]?.online">
|
||||
<span class="noc-metric-chip" :class="getTempClass(displayStatuses[display.ip_address])">
|
||||
<i class="fa-solid fa-temperature-half"></i>
|
||||
{{ displayStatuses[display.ip_address]?.temperature_c?.toFixed(1) || '--' }}°C
|
||||
</span>
|
||||
<span class="noc-metric-chip">
|
||||
<i class="fa-solid fa-microchip"></i>
|
||||
{{ displayStatuses[display.ip_address]?.cpu_percent?.toFixed(0) || '--' }}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="noc-actions">
|
||||
<button class="noc-action-btn" @click="openEditModal(display)" title="Bearbeiten">
|
||||
<i class="fa-solid fa-pencil"></i>
|
||||
</button>
|
||||
<button class="noc-action-btn" @click="refreshDisplay(display)" :disabled="refreshingDisplays[display.id]" title="Aktualisieren">
|
||||
<i class="fa-solid fa-arrows-rotate" :class="{ 'fa-spin': refreshingDisplays[display.id] }"></i>
|
||||
</button>
|
||||
<button class="noc-action-btn" @click="togglePower(display)" title="Ein/Aus">
|
||||
<i class="fa-solid fa-power-off"></i>
|
||||
</button>
|
||||
<button class="noc-action-btn danger" @click="confirmDelete(display)" title="Löschen">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="displays.length === 0" class="noc-group-empty">
|
||||
Displays hierher ziehen
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="noc-add-group" @click="openAddGroupModal">
|
||||
<i class="fa-solid fa-plus"></i>
|
||||
<span>Gruppe hinzufügen</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Display Modal -->
|
||||
<tt-dialog :show="showDisplayModal" :title="editingDisplay ? 'Edit Display' : 'Add Display'" @close="closeDisplayModal" size="normal">
|
||||
<tt-dialog :show="showDisplayModal" :title="editingDisplay ? 'Display bearbeiten' : 'Display hinzufügen'" @close="closeDisplayModal" size="normal">
|
||||
<div class="noc-modal-form">
|
||||
<!-- Discover Pi Section (only for new displays) -->
|
||||
<div v-if="!editingDisplay" class="noc-discover-section">
|
||||
<h4>Discover Raspberry Pi</h4>
|
||||
<h4>Raspberry Pi suchen</h4>
|
||||
<div class="noc-discover-row">
|
||||
<input type="text" v-model="discoverIp" placeholder="IP Address (e.g. 192.168.1.100)" class="form-control" />
|
||||
<input type="text" v-model="discoverIp" placeholder="IP-Adresse (z.B. 192.168.1.100)" class="form-control" />
|
||||
<input type="number" v-model="discoverPort" placeholder="Port" class="form-control" style="width: 100px;" />
|
||||
<button class="ghost-btn" @click="discoverPi" :disabled="discovering">
|
||||
<i class="fa-duotone fa-radar" :class="{ 'fa-spin': discovering }"></i>
|
||||
Discover
|
||||
Suchen
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="discoveredPi" class="noc-discovered-info">
|
||||
<div class="noc-discovered-header">
|
||||
<i class="fa-duotone fa-check-circle" style="color: var(--tt-ok);"></i>
|
||||
<span>{{ discoveredPi.hostname || 'Raspberry Pi' }} found</span>
|
||||
<span>{{ discoveredPi.hostname || 'Raspberry Pi' }} gefunden</span>
|
||||
</div>
|
||||
<div class="noc-discovered-displays">
|
||||
<div v-for="(disp, idx) in discoveredPi.displays" :key="idx" class="noc-discovered-display" @click="selectDiscoveredDisplay(disp, idx)">
|
||||
<span class="noc-hdmi-badge">HDMI:{{ disp.hdmi_port }}</span>
|
||||
<span class="noc-url-preview">{{ truncateUrl(disp.current_url) || 'No URL' }}</span>
|
||||
<span class="noc-url-preview">{{ truncateUrl(disp.current_url) || 'Keine URL' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,14 +195,13 @@ const RaspberryDisplay = {
|
||||
<hr class="noc-divider" />
|
||||
</div>
|
||||
|
||||
<!-- Form Fields -->
|
||||
<div class="noc-form-group">
|
||||
<label>Display Label *</label>
|
||||
<input type="text" v-model="formData.display_label" class="form-control" placeholder="e.g. NOC Monitor 1" />
|
||||
<label>Bezeichnung *</label>
|
||||
<input type="text" v-model="formData.display_label" class="form-control" placeholder="z.B. NOC Monitor 1" />
|
||||
</div>
|
||||
<div class="noc-form-row">
|
||||
<div class="noc-form-group">
|
||||
<label>IP Address *</label>
|
||||
<label>IP-Adresse *</label>
|
||||
<input type="text" v-model="formData.ip_address" class="form-control" placeholder="192.168.1.100" />
|
||||
</div>
|
||||
<div class="noc-form-group" style="width: 120px;">
|
||||
@@ -231,12 +215,12 @@ const RaspberryDisplay = {
|
||||
</div>
|
||||
<div class="noc-form-row">
|
||||
<div class="noc-form-group">
|
||||
<label>Group *</label>
|
||||
<label>Gruppe *</label>
|
||||
<select v-model="formData.group_name" class="form-control">
|
||||
<option v-for="group in availableGroups" :key="group" :value="group">{{ group }}</option>
|
||||
<option value="__new__">+ New Group...</option>
|
||||
<option value="__new__">+ Neue Gruppe...</option>
|
||||
</select>
|
||||
<input v-if="formData.group_name === '__new__'" type="text" v-model="newGroupName" class="form-control mt-2" placeholder="Enter new group name" />
|
||||
<input v-if="formData.group_name === '__new__'" type="text" v-model="newGroupName" class="form-control mt-2" placeholder="Gruppenname eingeben" />
|
||||
</div>
|
||||
<div class="noc-form-group" style="width: 120px;">
|
||||
<label>HDMI Port</label>
|
||||
@@ -247,7 +231,7 @@ const RaspberryDisplay = {
|
||||
</div>
|
||||
</div>
|
||||
<div class="noc-form-group">
|
||||
<label>Monitor Size</label>
|
||||
<label>Monitorgröße</label>
|
||||
<select v-model="formData.monitor_size" class="form-control">
|
||||
<option value="27">27"</option>
|
||||
<option value="42">42"</option>
|
||||
@@ -261,44 +245,41 @@ const RaspberryDisplay = {
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<button class="ghost-btn" @click="closeDisplayModal">Cancel</button>
|
||||
<button class="ghost-btn" @click="closeDisplayModal">Abbrechen</button>
|
||||
<button class="primary-btn" @click="saveDisplay" :disabled="saving">
|
||||
<i v-if="saving" class="fa-duotone fa-spinner fa-spin"></i>
|
||||
{{ editingDisplay ? 'Save Changes' : 'Add Display' }}
|
||||
{{ editingDisplay ? 'Speichern' : 'Hinzufügen' }}
|
||||
</button>
|
||||
</template>
|
||||
</tt-dialog>
|
||||
|
||||
<!-- Add Group Modal -->
|
||||
<tt-dialog :show="showAddGroupModal" title="Add New Group" @close="showAddGroupModal = false">
|
||||
<tt-dialog :show="showAddGroupModal" title="Neue Gruppe erstellen" @close="showAddGroupModal = false">
|
||||
<div class="noc-form-group">
|
||||
<label>Group Name</label>
|
||||
<input type="text" v-model="newGroupNameInput" class="form-control" placeholder="e.g. Conference Room" @keyup.enter="createGroup" />
|
||||
<label>Gruppenname</label>
|
||||
<input type="text" v-model="newGroupNameInput" class="form-control" placeholder="z.B. Konferenzraum" @keyup.enter="createGroup" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<button class="ghost-btn" @click="showAddGroupModal = false">Cancel</button>
|
||||
<button class="primary-btn" @click="createGroup" :disabled="!newGroupNameInput.trim()">Create Group</button>
|
||||
<button class="ghost-btn" @click="showAddGroupModal = false">Abbrechen</button>
|
||||
<button class="primary-btn" @click="createGroup" :disabled="!newGroupNameInput.trim()">Erstellen</button>
|
||||
</template>
|
||||
</tt-dialog>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<tt-dialog :show="showDeleteModal" title="Delete Display" @close="showDeleteModal = false">
|
||||
<p>Are you sure you want to delete <strong>{{ deleteTarget?.display_label }}</strong>?</p>
|
||||
<p class="text-muted small">This action cannot be undone.</p>
|
||||
<tt-dialog :show="showDeleteModal" title="Display löschen" @close="showDeleteModal = false">
|
||||
<p>Möchten Sie <strong>{{ deleteTarget?.display_label }}</strong> wirklich löschen?</p>
|
||||
<p class="text-muted small">Diese Aktion kann nicht rückgängig gemacht werden.</p>
|
||||
<template #footer>
|
||||
<button class="ghost-btn" @click="showDeleteModal = false">Cancel</button>
|
||||
<button class="ghost-btn" @click="showDeleteModal = false">Abbrechen</button>
|
||||
<button class="primary-btn danger" @click="deleteDisplay" :disabled="deleting">
|
||||
<i v-if="deleting" class="fa-duotone fa-spinner fa-spin"></i>
|
||||
Delete
|
||||
Löschen
|
||||
</button>
|
||||
</template>
|
||||
</tt-dialog>
|
||||
|
||||
<!-- Sequential Refresh Progress -->
|
||||
<div v-if="refreshingAll && refreshProgress.total > 0" class="noc-progress-overlay">
|
||||
<div class="noc-progress-header">
|
||||
<i class="fa-duotone fa-arrows-rotate fa-spin"></i>
|
||||
<span>Refreshing Displays</span>
|
||||
<span>Displays werden aktualisiert</span>
|
||||
</div>
|
||||
<div class="noc-progress-bar">
|
||||
<div class="noc-progress-fill" :style="{ width: (refreshProgress.current / refreshProgress.total * 100) + '%' }"></div>
|
||||
@@ -400,7 +381,7 @@ const RaspberryDisplay = {
|
||||
|
||||
async loadDisplays() {
|
||||
try {
|
||||
const response = await axios.get(window.TT_CONFIG.BASE_URL + 'api', {
|
||||
const response = await axios.get(window.TT_CONFIG.BASE_URL + '/api', {
|
||||
params: { do: 'getDisplays' }
|
||||
});
|
||||
if (response.data.status === 'OK') {
|
||||
@@ -416,7 +397,7 @@ const RaspberryDisplay = {
|
||||
|
||||
async loadStatuses() {
|
||||
try {
|
||||
const response = await axios.get(window.TT_CONFIG.BASE_URL + 'api', {
|
||||
const response = await axios.get(window.TT_CONFIG.BASE_URL + '/api', {
|
||||
params: { do: 'getBatchStatus' }
|
||||
});
|
||||
if (response.data.status === 'OK') {
|
||||
@@ -506,13 +487,13 @@ const RaspberryDisplay = {
|
||||
if (groupName === '__new__') {
|
||||
groupName = this.newGroupName.trim();
|
||||
if (!groupName) {
|
||||
alert('Please enter a group name');
|
||||
alert('Bitte geben Sie einen Gruppennamen ein');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.formData.display_label || !this.formData.ip_address || !groupName) {
|
||||
alert('Please fill in all required fields');
|
||||
alert('Bitte füllen Sie alle Pflichtfelder aus');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -528,16 +509,16 @@ const RaspberryDisplay = {
|
||||
params.id = this.editingDisplay.id;
|
||||
}
|
||||
|
||||
const response = await axios.post(window.TT_CONFIG.BASE_URL + 'api', null, { params });
|
||||
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('Failed to save display');
|
||||
alert('Speichern fehlgeschlagen');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save display:', error);
|
||||
alert('Failed to save display');
|
||||
alert('Speichern fehlgeschlagen');
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
@@ -553,7 +534,7 @@ const RaspberryDisplay = {
|
||||
|
||||
this.deleting = true;
|
||||
try {
|
||||
const response = await axios.post(window.TT_CONFIG.BASE_URL + 'api', null, {
|
||||
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') {
|
||||
@@ -577,7 +558,7 @@ const RaspberryDisplay = {
|
||||
this.discoverError = '';
|
||||
|
||||
try {
|
||||
const response = await axios.post(window.TT_CONFIG.BASE_URL + 'api', null, {
|
||||
const response = await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, {
|
||||
params: {
|
||||
do: 'discoverPi',
|
||||
ip_address: this.discoverIp,
|
||||
@@ -590,10 +571,10 @@ const RaspberryDisplay = {
|
||||
this.formData.agent_port = this.discoverPort;
|
||||
this.formData.hostname = this.discoveredPi.hostname || '';
|
||||
} else {
|
||||
this.discoverError = response.data.result?.error || 'Could not connect to Raspberry Pi';
|
||||
this.discoverError = response.data.result?.error || 'Verbindung zum Raspberry Pi fehlgeschlagen';
|
||||
}
|
||||
} catch (error) {
|
||||
this.discoverError = 'Failed to discover Pi';
|
||||
this.discoverError = 'Suche fehlgeschlagen';
|
||||
} finally {
|
||||
this.discovering = false;
|
||||
}
|
||||
@@ -633,7 +614,7 @@ const RaspberryDisplay = {
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(window.TT_CONFIG.BASE_URL + 'api', null, {
|
||||
const response = await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, {
|
||||
params: {
|
||||
do: 'setUrl',
|
||||
id: display.id,
|
||||
@@ -654,7 +635,7 @@ const RaspberryDisplay = {
|
||||
async refreshDisplay(display) {
|
||||
this.refreshingDisplays[display.id] = true;
|
||||
try {
|
||||
await axios.post(window.TT_CONFIG.BASE_URL + 'api', null, {
|
||||
await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, {
|
||||
params: { do: 'refreshDisplay', id: display.id }
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -669,7 +650,7 @@ const RaspberryDisplay = {
|
||||
const state = status?.displays?.[display.hdmi_port]?.cec_state === 'on' ? 'off' : 'on';
|
||||
|
||||
try {
|
||||
await axios.post(window.TT_CONFIG.BASE_URL + 'api', null, {
|
||||
await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, {
|
||||
params: { do: 'cecPower', id: display.id, state }
|
||||
});
|
||||
await this.loadStatuses();
|
||||
@@ -691,7 +672,7 @@ const RaspberryDisplay = {
|
||||
this.refreshingDisplays[display.id] = true;
|
||||
|
||||
try {
|
||||
await axios.post(window.TT_CONFIG.BASE_URL + 'api', null, {
|
||||
await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, {
|
||||
params: { do: 'refreshDisplay', id: display.id }
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
@@ -717,7 +698,7 @@ const RaspberryDisplay = {
|
||||
async powerAllDisplays(state) {
|
||||
this.poweringAll = true;
|
||||
try {
|
||||
await axios.post(window.TT_CONFIG.BASE_URL + 'api', null, {
|
||||
await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, {
|
||||
params: { do: 'powerAll', state }
|
||||
});
|
||||
await this.loadStatuses();
|
||||
@@ -729,11 +710,11 @@ const RaspberryDisplay = {
|
||||
},
|
||||
|
||||
async rebootAllPis() {
|
||||
if (!confirm('Are you sure you want to reboot all Raspberry Pis?')) return;
|
||||
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, {
|
||||
await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, {
|
||||
params: { do: 'rebootAll' }
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -788,7 +769,7 @@ const RaspberryDisplay = {
|
||||
|
||||
// Persist to server
|
||||
try {
|
||||
await axios.post(window.TT_CONFIG.BASE_URL + 'api', null, {
|
||||
await axios.post(window.TT_CONFIG.BASE_URL + '/api', null, {
|
||||
params: {
|
||||
do: 'updateDisplay',
|
||||
id: display.id,
|
||||
|
||||
Reference in New Issue
Block a user