269 lines
11 KiB
JavaScript
269 lines
11 KiB
JavaScript
const RadiusAVMScanner = {
|
|
name: 'RadiusAVMScanner',
|
|
template: `
|
|
<div class="tt-scope">
|
|
<div class="filters-layout" style="margin-bottom: 16px;">
|
|
<div class="field">
|
|
<div class="input-wrap">
|
|
<i class="fa-duotone fa-router input-icon"></i>
|
|
<select class="ri" v-model="deviceTypeFilter" style="padding-left: 36px;">
|
|
<option value="">Alle Gerätetypen</option>
|
|
<option v-for="dt in deviceTypes" :key="dt" :value="dt">{{ dt }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="field">
|
|
<label class="switch-field">
|
|
<span class="mini muted">Erledigte anzeigen</span>
|
|
<span class="switch">
|
|
<input type="checkbox" v-model="showErledigt">
|
|
<span class="switch-track">
|
|
<i class="fa-duotone fa-eye on"></i>
|
|
<i class="fa-duotone fa-eye-slash off"></i>
|
|
</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
<div class="cluster" style="gap: 8px;">
|
|
<button
|
|
class="primary-btn"
|
|
@click="startScan"
|
|
:disabled="state?.scanning"
|
|
v-if="!state?.scanning"
|
|
>
|
|
<i class="fa-duotone fa-play"></i> Scan starten
|
|
</button>
|
|
<button
|
|
class="danger-btn"
|
|
@click="stopScan"
|
|
v-if="state?.scanning"
|
|
>
|
|
<i class="fa-duotone fa-stop"></i> Scan stoppen
|
|
</button>
|
|
<button class="ghost-btn" @click="refreshState" :disabled="isLoading" data-tooltip="Aktualisieren">
|
|
<i class="fa-duotone fa-refresh" :class="{ 'fa-spin': isLoading }"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="state?.scanning" class="progress-container" style="margin-bottom: 16px;">
|
|
<div class="card" style="padding: 16px;">
|
|
<div class="cluster" style="justify-content: space-between; margin-bottom: 8px;">
|
|
<span class="fw-500">Scan läuft...</span>
|
|
<span class="mono">{{ state.progress?.current || 0 }} / {{ state.progress?.total || 0 }}</span>
|
|
</div>
|
|
<div class="progress-bar" style="height: 8px; background: var(--tt-bg-muted); border-radius: 4px; overflow: hidden;">
|
|
<div
|
|
class="progress-fill"
|
|
:style="{ width: progressPercent + '%', background: 'var(--tt-primary)', height: '100%', transition: 'width 0.3s' }"
|
|
></div>
|
|
</div>
|
|
<div v-if="state.currentDevice" class="mt-between" style="font-size: 13px; color: var(--tt-text-muted);">
|
|
Aktuell: <span class="mono">{{ state.currentDevice.mac }}</span>
|
|
<span v-if="state.currentDevice.ip"> ({{ state.currentDevice.ip }})</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="results-container">
|
|
<tt-data-table
|
|
:items="filteredDevices"
|
|
:is-loading="isLoading && !state"
|
|
:has-searched="true"
|
|
:skeleton-row-count="6"
|
|
initial-placeholder-text="Keine Geräte gescannt. Klicken Sie auf 'Scan starten'."
|
|
>
|
|
<template #head>
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 170px;">MAC-Adresse</th>
|
|
<th style="width: 140px;">IP-Adresse</th>
|
|
<th style="width: 150px;">Kunde</th>
|
|
<th>Gerätetyp</th>
|
|
<th style="width: 80px; text-align: center;">Port</th>
|
|
<th style="width: 80px; text-align: center;">Erledigt</th>
|
|
<th style="width: 120px;">Gescannt</th>
|
|
</tr>
|
|
</thead>
|
|
</template>
|
|
<template #skeleton-row>
|
|
<td><tt-skeleton /></td>
|
|
<td><tt-skeleton /></td>
|
|
<td><tt-skeleton /></td>
|
|
<td><tt-skeleton /></td>
|
|
<td><tt-skeleton /></td>
|
|
<td><tt-skeleton /></td>
|
|
<td><tt-skeleton /></td>
|
|
</template>
|
|
<template #row="{ item }">
|
|
<td class="mono nowrap">
|
|
<tt-copy-button :text="item.mac" tooltip-align="right" />
|
|
{{ item.mac }}
|
|
</td>
|
|
<td class="mono">
|
|
<template v-if="item.ip">
|
|
<tt-copy-button :text="item.ip" tooltip-align="right" />
|
|
{{ item.ip }}
|
|
</template>
|
|
<span v-else class="muted">—</span>
|
|
</td>
|
|
<td>
|
|
<a
|
|
v-if="item.customerId"
|
|
class="link"
|
|
target="_blank"
|
|
:href="window.TT_CONFIG.BASE_PATH + '/Address?filter%5Bcustomer_number%5D=' + item.customerId"
|
|
>
|
|
{{ item.customerId }}
|
|
</a>
|
|
<div v-if="item.customerName" class="mini muted clamp-1">{{ item.customerName }}</div>
|
|
</td>
|
|
<td>
|
|
<span v-if="item.deviceType" class="fw-500">{{ item.deviceType }}</span>
|
|
<span v-else-if="item.error" class="text-danger mini">{{ item.error }}</span>
|
|
<span v-else class="muted">—</span>
|
|
<div v-if="item.isRepeater" class="mini muted">Repeater</div>
|
|
<div v-if="item.isGateway" class="mini muted">Gateway</div>
|
|
</td>
|
|
<td class="mono" style="text-align: center;">
|
|
<span v-if="item.port">{{ item.port }}</span>
|
|
<span v-else class="muted">—</span>
|
|
</td>
|
|
<td style="text-align: center;">
|
|
<label class="switch">
|
|
<input type="checkbox" :checked="item.erledigt" @change="toggleErledigt(item.mac)">
|
|
<span class="switch-track">
|
|
<i class="fa-duotone fa-check on"></i>
|
|
<i class="fa-duotone fa-xmark off"></i>
|
|
</span>
|
|
</label>
|
|
</td>
|
|
<td class="mini muted nowrap">
|
|
{{ formatDate(item.scannedAt) }}
|
|
</td>
|
|
</template>
|
|
</tt-data-table>
|
|
|
|
<div class="results-summary" style="margin-top: 12px;">
|
|
<span v-if="state?.devices?.length">
|
|
{{ filteredDevices.length }} von {{ state.devices.length }} Geräten
|
|
<template v-if="state.scanning"> (Scan läuft...)</template>
|
|
</span>
|
|
<span v-else class="muted">Keine Geräte</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
data: () => ({
|
|
window: window,
|
|
state: null,
|
|
isLoading: false,
|
|
polling: null,
|
|
deviceTypeFilter: '',
|
|
showErledigt: true
|
|
}),
|
|
computed: {
|
|
progressPercent() {
|
|
if (!this.state?.progress?.total) return 0;
|
|
return Math.round((this.state.progress.current / this.state.progress.total) * 100);
|
|
},
|
|
deviceTypes() {
|
|
if (!this.state?.devices) return [];
|
|
const types = new Set();
|
|
this.state.devices.forEach(d => {
|
|
if (d.deviceType) types.add(d.deviceType);
|
|
});
|
|
return Array.from(types).sort();
|
|
},
|
|
filteredDevices() {
|
|
if (!this.state?.devices) return [];
|
|
return this.state.devices.filter(d => {
|
|
if (this.deviceTypeFilter && d.deviceType !== this.deviceTypeFilter) return false;
|
|
if (!this.showErledigt && d.erledigt) return false;
|
|
return true;
|
|
});
|
|
}
|
|
},
|
|
methods: {
|
|
initIfNeeded() {
|
|
this.refreshState();
|
|
},
|
|
startPolling() {
|
|
if (this.polling) return;
|
|
this.polling = setInterval(() => {
|
|
this.refreshState(true);
|
|
}, 3000); // Poll every 3 seconds
|
|
},
|
|
stopPolling() {
|
|
if (this.polling) {
|
|
clearInterval(this.polling);
|
|
this.polling = null;
|
|
}
|
|
},
|
|
async refreshState(silent = false) {
|
|
if (!silent) this.isLoading = true;
|
|
try {
|
|
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/Radius/avmScannerGetState`);
|
|
this.state = data;
|
|
|
|
// Start/stop polling based on scanning state
|
|
if (data.scanning && !this.polling) {
|
|
this.startPolling();
|
|
} else if (!data.scanning && this.polling) {
|
|
this.stopPolling();
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to fetch AVM scanner state:', e);
|
|
}
|
|
if (!silent) this.isLoading = false;
|
|
},
|
|
async startScan() {
|
|
try {
|
|
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/Radius/avmScannerStart`);
|
|
if (data.success) {
|
|
window.notify('success', `Scan gestartet für ${data.total} Geräte`);
|
|
this.startPolling(); // Start polling immediately
|
|
} else {
|
|
window.notify('warning', data.message || 'Scan konnte nicht gestartet werden');
|
|
}
|
|
this.refreshState();
|
|
} catch (e) {
|
|
console.error('Failed to start scan:', e);
|
|
window.notify('error', 'Fehler beim Starten des Scans');
|
|
}
|
|
},
|
|
async stopScan() {
|
|
try {
|
|
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/Radius/avmScannerStop`);
|
|
if (data.success) {
|
|
window.notify('info', 'Scan wird gestoppt...');
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to stop scan:', e);
|
|
window.notify('error', 'Fehler beim Stoppen des Scans');
|
|
}
|
|
},
|
|
async toggleErledigt(mac) {
|
|
try {
|
|
await axios.post(`${window.TT_CONFIG.BASE_PATH}/Radius/avmScannerToggleErledigt`, { mac });
|
|
this.refreshState(true);
|
|
} catch (e) {
|
|
console.error('Failed to toggle erledigt:', e);
|
|
window.notify('error', 'Fehler beim Aktualisieren');
|
|
}
|
|
},
|
|
formatDate(dateStr) {
|
|
if (!dateStr) return '—';
|
|
const d = new Date(dateStr);
|
|
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
},
|
|
beforeUnmount() {
|
|
this.stopPolling();
|
|
}
|
|
};
|
|
|
|
if (window.VueApp) {
|
|
window.VueApp.component('radius-avm-scanner', RadiusAVMScanner);
|
|
}
|