1529 lines
78 KiB
JavaScript
1529 lines
78 KiB
JavaScript
Vue.component('radius-users', {
|
||
template: `
|
||
<div class="radius-scope">
|
||
<div class="filters-layout">
|
||
<div class="field">
|
||
<radius-autocomplete v-model="billAddrDisplay" :wide="true" placeholder="Kunde suchen"
|
||
@select="onAddrSelect" @enter="loadRadiusUsers" @mode-change="onModeChange"/>
|
||
</div>
|
||
<div class="field">
|
||
<div class="input-wrap ip-field-wrapper"><span class="ip-focus-tooltip">z.B. nat* für lazy Suche</span><i
|
||
class="fa-duotone fa-user input-icon"></i><input class="ri" v-model="username" placeholder="Username"
|
||
@keydown.enter="loadRadiusUsers" autocomplete="off"
|
||
autocapitalize="none" autocorrect="off"
|
||
inputmode="text" name="radius-username"/></div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="input-wrap ip-field-wrapper"><span class="ip-focus-tooltip">Prefixe: '=' exakt, '*' Verlauf (lazy), '*=' Verlauf (exakt)</span>
|
||
<div class="input-wrap"><i class="fa-duotone fa-network-wired input-icon"></i><input class="ri"
|
||
v-model="ip"
|
||
placeholder="IP-Adresse"
|
||
@keydown.enter="loadRadiusUsers"
|
||
autocomplete="off"
|
||
autocapitalize="none"
|
||
autocorrect="off"
|
||
inputmode="decimal"
|
||
name="radius-ip"/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<div class="input-wrap"><i class="fa-duotone fa-note-sticky input-icon"></i><input class="ri" v-model="info"
|
||
placeholder="Info"
|
||
@keydown.enter="loadRadiusUsers"/>
|
||
</div>
|
||
</div>
|
||
<div class="cluster" style="gap: 8px;">
|
||
<div class="field"><label class="switch-field"><span class="mini muted">Online-Status</span><span
|
||
class="switch"><input type="checkbox" v-model="checkOnlineState"><span class="switch-track"><i
|
||
class="fa-duotone fa-signal-bars-good on"></i><i class="fa-duotone fa-signal-bars-slash off"></i></span></span></label>
|
||
</div>
|
||
<button class="primary-btn" @click="loadRadiusUsers" :disabled="isLoading" style="flex-grow: 1;"><span
|
||
v-if="!isLoading"><i class="fa-duotone fa-magnifying-glass"></i></span><span v-else
|
||
class="btn-loader"></span>
|
||
</button>
|
||
<button class="danger-btn" @click="clearFilters" :disabled="!hasFilters" data-tooltip="Eingaben leeren"
|
||
data-tooltip-align="left"><i class="fa-duotone fa-xmark"></i></button>
|
||
</div>
|
||
</div>
|
||
<div class="results-container mt-between">
|
||
<radius-table-view :items="visibleUsers" :is-loading="isLoading" :has-searched="hasSearched"
|
||
:skeleton-row-count="6"
|
||
initial-placeholder-text="Beginnen Sie Ihre Suche, indem Sie Filter eingeben.">
|
||
<template #head>
|
||
<thead>
|
||
<tr>
|
||
<th style="text-align: center; width: 170px;">Kundennummer</th>
|
||
<th style="text-align: center; width: 183px;">Username</th>
|
||
<th style="text-align: center">Info</th>
|
||
<th style="text-align: center; width: 190px;">Status</th>
|
||
<th style="text-align: center; width: 180px;">Aktionen</th>
|
||
</tr>
|
||
</thead>
|
||
</template>
|
||
<template #skeleton-row>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line" style="height: 36px;"></div>
|
||
</td>
|
||
</template>
|
||
<template #row="{ item }">
|
||
<td><a class="link" target="_blank"
|
||
:href="window.TT_CONFIG.BASE_PATH + '/Address?filter%5Bcustomer_number%5D=' + item.customerNumber"
|
||
data-tooltip="Kunden in neuem Tab öffnen" data-tooltip-align="right">{{ item.customerNumber }}</a>
|
||
</td>
|
||
<td class="nowrap"><a class="link" target="_blank"
|
||
:href="'http://radius.xinon.at/edit_user.php?user=' + item.username"
|
||
data-tooltip="User in Radius öffnen"
|
||
data-tooltip-align="right">{{ item.username }}</a>
|
||
<button class="icon-btn sm" data-tooltip="Kopieren" data-tooltip-align="right"
|
||
@click="copy(item.username, $event)"><i class="fa-duotone fa-copy copy-icon"></i><i
|
||
class="fa-duotone fa-check check-icon"></i></button>
|
||
</td>
|
||
<td class="mono clamp-2">{{ item.info || '—' }}</td>
|
||
<td>
|
||
<radius-online-state
|
||
v-if="checkOnlineState"
|
||
:username="item.username"
|
||
:key="item.username + '_'+searchCount"
|
||
@scan-ip="onScanIp($event, item)"
|
||
/>
|
||
</td>
|
||
<td class="nowrap cluster" style="gap: 4px; justify-content: center;">
|
||
<button class="ghost-btn" @click="fetchRadacctData(item.username)" data-tooltip="Details"><i
|
||
class="fa-duotone fa-circle-info"></i></button>
|
||
<button class="ghost-btn" @click="openTransferModal(item.username)" data-tooltip="Transfer Statistik"
|
||
data-tooltip-align="left"><i class="fa-duotone fa-chart-line"></i></button>
|
||
<button v-if="window.TT_CONFIG.ACS_ENABLED" class="ghost-btn" @click="openRouterManagement(item)" data-tooltip="Router Management"
|
||
data-tooltip-align="left"><i class="fa-duotone fa-router"></i></button>
|
||
</td>
|
||
</template>
|
||
<template #observer>
|
||
<div ref="sentinel" style="height: 1px;"></div>
|
||
</template>
|
||
</radius-table-view>
|
||
<div v-if="hasSearched" class="results-summary"><span v-if="isLoading">Suche läuft...</span><span
|
||
v-else-if="radiusUsers.length">{{ radiusUsers.length }} Treffer gefunden</span></div>
|
||
</div>
|
||
<radius-modal :show="showRadacctModal" title="RADIUS Daten" @close="showRadacctModal=false">
|
||
<div class="kv-redesign">
|
||
<div class="kv-row"><span class="kv-label">Status</span>
|
||
<div class="kv-value">
|
||
<div v-if="radacctData"><strong class="chip"
|
||
:class="radacctData.online ? 'ok' : 'bad'">{{ radacctData.online ? 'Online' : 'Offline' }}</strong>
|
||
</div>
|
||
<div v-else>
|
||
<div class="skeleton-line" style="width: 80px; --h: 24px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kv-row"><span class="kv-label">IP</span>
|
||
<div class="kv-value">
|
||
<div v-if="radacctData" class="inline-copy"><code v-if="radacctData.ip">{{ radacctData.ip }}</code><code
|
||
v-else>—</code>
|
||
<button v-if="radacctData.ip" class="icon-btn sm" @click="copy(radacctData.ip, $event)"
|
||
data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i
|
||
class="fa-duotone fa-check check-icon"></i></button>
|
||
</div>
|
||
<div v-else>
|
||
<div class="skeleton-line" style="width: 120px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kv-row"><span class="kv-label">Username</span>
|
||
<div class="kv-value">
|
||
<div v-if="radacctData" class="inline-copy"><a class="link" target="_blank"
|
||
:href="'http://radius.xinon.at/edit_user.php?user=' + radacctData.username"
|
||
data-tooltip="User in Radius öffnen">{{ radacctData.username }}</a>
|
||
<button class="icon-btn sm" @click="copy(radacctData.username, $event)" data-tooltip="Kopieren"><i
|
||
class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
|
||
</div>
|
||
<div v-else>
|
||
<div class="skeleton-line" style="width: 150px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<template v-if="radacctData">
|
||
<div class="kv-row"><span class="kv-label">Kundennummer</span><code
|
||
class="kv-value">{{ radacctData.customerNumber || '—' }}</code></div>
|
||
<div class="kv-row"><span class="kv-label">Kundenname</span>
|
||
<div class="kv-value clamp-2">{{ radacctData.customerName || '—' }}</div>
|
||
</div>
|
||
<div class="kv-row"><span class="kv-label">Info</span>
|
||
<div class="kv-value clamp-3 mono small">{{ radacctData.info || '—' }}</div>
|
||
</div>
|
||
<div class="kv-row"><span class="kv-label">WLAN Password</span><code
|
||
class="kv-value mono">{{ radacctData.wlanPassword || '—' }}</code></div>
|
||
<div class="kv-row"><span class="kv-label">Bandbreite</span><code
|
||
class="kv-value">{{ radacctData.actualBandwidth || '—' }}</code></div>
|
||
</template>
|
||
<template v-else>
|
||
<div class="kv-row"><span class="kv-label">Kundennummer</span>
|
||
<div class="kv-value">
|
||
<div class="skeleton-line" style="width: 70px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
<div class="kv-row"><span class="kv-label">Kundenname</span>
|
||
<div class="kv-value">
|
||
<div class="skeleton-line" style="width: 200px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
<div class="kv-row"><span class="kv-label">Info</span>
|
||
<div class="kv-value">
|
||
<div class="skeleton-line" style="--h:14px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
<div class="kv-row"><span class="kv-label">WLAN Password</span>
|
||
<div class="kv-value">
|
||
<div class="skeleton-line" style="width: 100px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
<div class="kv-row"><span class="kv-label">Bandbreite</span>
|
||
<div class="kv-value">
|
||
<div class="skeleton-line" style="width: 180px; margin-left: auto;"></div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</radius-modal>
|
||
<radius-modal :show="showTransferModal" :title="'Transfer Statistik für ' + transferModalUsername"
|
||
@close="closeTransferModal" modal-class="modal-card-wide">
|
||
<div class="modal-body-scrollable">
|
||
<div v-if="transferYearlyData || transferInitialLoading">
|
||
<div class="unselectable">
|
||
<div class="cluster"
|
||
style="justify-content: space-between; margin-bottom: 16px; flex-wrap: nowrap; margin-top: 4px; padding-left: 8px;">
|
||
<div class="cluster">
|
||
<div class="custom-dropdown">
|
||
<button class="dropdown-toggle"
|
||
@click="!transferInitialLoading && (showYearDropdown = !showYearDropdown)"
|
||
:class="{'is-open': showYearDropdown}"><span>{{ transferYear }}</span><i
|
||
class="fa-solid fa-chevron-down"></i></button>
|
||
<transition name="ac-pop">
|
||
<div v-if="showYearDropdown" class="dropdown-panel">
|
||
<div v-for="y in availableYears" :key="y" class="dropdown-item" @click="selectYear(y)">
|
||
{{ y }}
|
||
</div>
|
||
</div>
|
||
</transition>
|
||
</div>
|
||
<div class="cluster" style="gap: 4px;">
|
||
<button v-for="m in allMonths" :key="m.month" class="tab-btn"
|
||
:class="{active: transferMonth === m.month}" :disabled="isMonthDisabled(m.month)"
|
||
@click="changeTransferMonth(m.month)">{{ m.name }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="cluster" style="gap: 16px;">
|
||
<div class="muted small mono" style="text-align: right; flex-shrink: 0;">Gesamt {{ transferYear }}
|
||
:<br><strong v-if="transferInitialLoading || !transferYearlyData">
|
||
<div class="skeleton-line" style="width: 110px; height: 16px; margin-left:auto;"></div>
|
||
</strong><strong
|
||
v-else>{{ window.RadiusUtils.formatBytes(transferYearlyData.yearlySummary.grandTotalBytes) }}</strong>
|
||
</div>
|
||
<button class="ghost-btn" @click="prepareEmailModal"
|
||
:disabled="transferInitialLoading || !transferYearlyData || !transferYearlyData.yearlySummary || transferYearlyData.yearlySummary.grandTotalBytes === 0"
|
||
data-tooltip="Statistik per E-Mail senden" data-tooltip-align="bottom-left"
|
||
data-tooltip-wrap="true"><i class="fa-duotone fa-paper-plane"></i></button>
|
||
</div>
|
||
</div>
|
||
<div class="grid g-4 cols-4">
|
||
<div class="stat-card-v2 stat-total">
|
||
<div class="stat-icon"><i class="fa-duotone fa-grid-2"></i></div>
|
||
<div>
|
||
<div class="stat-label">Monat gesamt</div>
|
||
<div class="stat-value"><span v-if="transferInitialLoading || transferMonthlyLoading"><div
|
||
class="skeleton-line" style="width: 100px; height: 18px;"></div></span><span
|
||
v-else>{{ window.RadiusUtils.formatBytes(transferMonthlyData?.summary?.grandTotalBytes || 0) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card-v2 stat-download">
|
||
<div class="stat-icon"><i class="fa-duotone fa-arrow-down-to-line"></i></div>
|
||
<div>
|
||
<div class="stat-label">Download</div>
|
||
<div class="stat-value"><span v-if="transferInitialLoading || transferMonthlyLoading"><div
|
||
class="skeleton-line" style="width: 100px; height: 18px;"></div></span><span
|
||
v-else>{{ window.RadiusUtils.formatBytes(transferMonthlyData?.summary?.totalDownloadBytes || 0) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card-v2 stat-upload">
|
||
<div class="stat-icon"><i class="fa-duotone fa-arrow-up-from-line"></i></div>
|
||
<div>
|
||
<div class="stat-label">Upload</div>
|
||
<div class="stat-value"><span v-if="transferInitialLoading || transferMonthlyLoading"><div
|
||
class="skeleton-line" style="width: 100px; height: 18px;"></div></span><span
|
||
v-else>{{ window.RadiusUtils.formatBytes(transferMonthlyData?.summary?.totalUploadBytes || 0) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="stat-card-v2 stat-duration">
|
||
<div class="stat-icon"><i class="fa-duotone fa-hourglass-clock"></i></div>
|
||
<div>
|
||
<div class="stat-label">Dauer</div>
|
||
<div class="stat-value"><span v-if="transferInitialLoading || transferMonthlyLoading"><div
|
||
class="skeleton-line" style="width: 80px; height: 18px;"></div></span><span
|
||
v-else>{{ window.RadiusUtils.formatDuration(transferMonthlyData?.summary?.totalDurationSeconds || 0) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="chart-card mt-3" style="height: 250px;">
|
||
<div v-if="transferMonthlyLoading || transferInitialLoading" class="chart-placeholder">
|
||
<div class="skeleton-line" style="width: 100%; height: 100%; border-radius: var(--radius);"></div>
|
||
</div>
|
||
<div
|
||
v-else-if="!transferMonthlyData || !transferMonthlyData.details || !transferMonthlyData.details.length"
|
||
class="chart-placeholder"><i class="fa-duotone fa-chart-pie"></i><span>Keine Daten in diesem Monat verfügbar</span>
|
||
</div>
|
||
<canvas
|
||
v-show="!transferMonthlyLoading && !transferInitialLoading && transferMonthlyData?.details?.length"
|
||
ref="transferChartCanvas"></canvas>
|
||
</div>
|
||
</div>
|
||
<div class="table-wrap mt-3" style="height: 350px;">
|
||
<div
|
||
v-if="!transferInitialLoading && !transferMonthlyLoading && (!transferMonthlyData || !transferMonthlyData.details || !transferMonthlyData.details.length)"
|
||
class="table-placeholder-fixed-height"><i class="fa-duotone fa-database"></i><span>Keine detaillierten Daten für diesen Monat.</span>
|
||
</div>
|
||
<table v-else class="tt-table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>Startzeit</th>
|
||
<th>Dauer</th>
|
||
<th>IP-Adresse</th>
|
||
<th style="text-align: right;">Download</th>
|
||
<th style="text-align: right;">Upload</th>
|
||
<th style="text-align: right;">Gesamt</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template v-if="transferInitialLoading || transferMonthlyLoading">
|
||
<tr v-for="n in 10" :key="'skel'+n">
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
<td>
|
||
<div class="skeleton-line"></div>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
<template v-else>
|
||
<tr v-for="(d, i) in transferMonthlyData.details" :key="i">
|
||
<td class="mono small">{{ d.startTime }}</td>
|
||
<td class="mono small">{{ window.RadiusUtils.formatDuration(d.durationSeconds) }}</td>
|
||
<td class="mono small">{{ d.ipAddress }}</td>
|
||
<td class="mono small" style="text-align: right;">
|
||
{{ window.RadiusUtils.formatBytes(d.downloadBytes) }}
|
||
</td>
|
||
<td class="mono small" style="text-align: right;">
|
||
{{ window.RadiusUtils.formatBytes(d.uploadBytes) }}
|
||
</td>
|
||
<td class="mono small" style="text-align: right;">
|
||
<strong>{{ window.RadiusUtils.formatBytes(d.totalBytes) }}</strong></td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
<div v-else-if="!transferInitialLoading" class="table-placeholder" style="min-height: 400px;"><i
|
||
class="fa-duotone fa-wifi-slash"></i>
|
||
<div>Daten konnten nicht geladen werden.</div>
|
||
</div>
|
||
</div>
|
||
</radius-modal>
|
||
<radius-modal :show="showEmailModal" title="Statistik per E-Mail senden" @close="showEmailModal=false">
|
||
<div>
|
||
<div class="field"><label style="margin-bottom: 8px; font-size: 14px;">Empfänger-E-Mail</label>
|
||
<div class="input-wrap"><i class="fa-duotone fa-envelope input-icon"></i><input class="ri" type="email"
|
||
v-model.trim="recipientEmail"
|
||
placeholder="name@domain.com"
|
||
@keydown.enter="isValidEmail && !isSendingEmail && sendTransferEmail()"
|
||
autocomplete="nope"
|
||
data-1p-ignore
|
||
data-lpignore="true"
|
||
data-form-type="other"
|
||
data-bwignore></div>
|
||
<p v-if="recipientEmail && !isValidEmail" class="muted small" style="color: var(--bad); margin-top: 4px;">
|
||
Bitte geben Sie eine gültige E-Mail-Adresse ein.</p></div>
|
||
<div class="cluster" style="justify-content: flex-end; margin-top: 24px; gap: 12px;">
|
||
<button class="ghost-btn" @click="showEmailModal=false" :disabled="isSendingEmail">Abbrechen</button>
|
||
<button class="primary-btn" @click="sendTransferEmail" :disabled="!isValidEmail || isSendingEmail"
|
||
style="min-width: 100px;"><span v-if="!isSendingEmail">Senden</span><span v-else
|
||
class="btn-loader"></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</radius-modal>
|
||
<radius-modal :show="showExtensionIdModal" title="Extension ID Konfigurieren" @close="showExtensionIdModal=false">
|
||
<div>
|
||
<div class="field"><label style="margin-bottom: 8px; font-size: 14px;">Chrome Extension ID</label>
|
||
<div class="input-wrap"><i class="fa-duotone fa-puzzle-piece input-icon"></i><input class="ri" type="text"
|
||
v-model.trim="extensionId"
|
||
placeholder="z.B. jglijfiddilckddlmbnlojmmlahboffh">
|
||
</div>
|
||
</div>
|
||
<div class="cluster" style="justify-content: flex-end; margin-top: 24px; gap: 12px;">
|
||
<button class="primary-btn" @click="saveExtensionId">Speichern</button>
|
||
</div>
|
||
</div>
|
||
</radius-modal>
|
||
|
||
<radius-modal :show="showRouterModal" :title="'Router Management - ' + (routerData.username || '')" @close="closeRouterModal" modal-class="modal-card-wide">
|
||
<div class="modal-body-scrollable">
|
||
<div v-if="routerLoading" class="table-placeholder" style="min-height: 300px;">
|
||
<div class="btn-loader" style="width: 50px; height: 50px;"></div>
|
||
<div style="margin-top: 16px;">Router wird gesucht...</div>
|
||
</div>
|
||
<div v-else-if="!routerDevice" class="table-placeholder" style="min-height: 300px;">
|
||
<i class="fa-duotone fa-router-slash" style="font-size: 48px; opacity: 0.3;"></i>
|
||
<div style="margin-top: 16px;">Kein Router mit dieser IP gefunden</div>
|
||
</div>
|
||
<div v-else>
|
||
<div style="padding: 8px;">
|
||
<div class="kv-redesign grid g-2 cols-2">
|
||
<div class="kv-row"><span class="kv-label">Hardware Version</span><code class="kv-value">{{ routerDevice.deviceInfo.hardwareVersion || '—' }}</code></div>
|
||
<div class="kv-row"><span class="kv-label">Software Version</span><code class="kv-value">{{ routerDevice.deviceInfo.softwareVersion || '—' }}</code></div>
|
||
<div class="kv-row full-width"><span class="kv-label">Seriennummer</span><code class="kv-value">{{ routerDevice.deviceInfo.serialNumber || '—' }}</code></div>
|
||
<div class="kv-row"><span class="kv-label">Device ID</span>
|
||
<div class="kv-value inline-copy">
|
||
<code>{{ routerDevice.deviceId || '—' }}</code>
|
||
<button v-if="routerDevice.deviceId" class="icon-btn sm" @click="copy(routerDevice.deviceId, $event)" data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
|
||
</div>
|
||
</div>
|
||
<div class="kv-row"><span class="kv-label">Externe IP</span>
|
||
<div class="kv-value inline-copy">
|
||
<code>{{ routerDevice.ip || '—' }}</code>
|
||
<button v-if="routerDevice.ip" class="icon-btn sm" @click="copy(routerDevice.ip, $event)" data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
|
||
</div>
|
||
</div>
|
||
<div class="kv-row"><span class="kv-label">Management IP</span>
|
||
<div class="kv-value inline-copy">
|
||
<code>{{ routerDevice.managementIp || '—' }}</code>
|
||
<button v-if="routerDevice.managementIp" class="icon-btn sm" @click="copy(routerDevice.managementIp, $event)" data-tooltip="Kopieren"><i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-3">
|
||
<h4 style="margin-bottom: 12px; font-size: 14px; font-weight: 600;">Router Aktionen</h4>
|
||
<div class="grid g-2 cols-3">
|
||
<button class="ghost-btn" @click="runRemoteAccess" :disabled="routerActionLoading || speedtestLoading">
|
||
<i class="fa-duotone fa-key"></i> Remote-Zugriff
|
||
</button>
|
||
<button class="ghost-btn" @click="refreshRouter" :disabled="routerActionLoading || speedtestLoading">
|
||
<i class="fa-duotone fa-arrows-rotate"></i> Aktualisieren
|
||
</button>
|
||
<button class="ghost-btn" @click="rebootRouter" :disabled="routerActionLoading || speedtestLoading">
|
||
<i class="fa-duotone fa-power-off"></i> Neustart
|
||
</button>
|
||
<button class="ghost-btn" @click="pingRouter" :disabled="routerActionLoading || speedtestLoading">
|
||
<i class="fa-duotone fa-signal-bars"></i> Ping
|
||
</button>
|
||
<button class="ghost-btn" @click="showParameterModal = true" :disabled="routerActionLoading || speedtestLoading">
|
||
<i class="fa-duotone fa-sliders"></i> Parameter lesen
|
||
</button>
|
||
<button class="ghost-btn" @click="showSetParameterModal = true" :disabled="routerActionLoading || speedtestLoading">
|
||
<i class="fa-duotone fa-pen-to-square"></i> Parameter setzen
|
||
</button>
|
||
<button class="ghost-btn" @click="runSpeedtest" :disabled="routerActionLoading || speedtestLoading">
|
||
<i class="fa-duotone fa-gauge-high"></i> Speedtest
|
||
</button>
|
||
<button class="danger-btn" @click="confirmFactoryReset" :disabled="routerActionLoading || speedtestLoading">
|
||
<i class="fa-duotone fa-triangle-exclamation"></i> Factory Reset
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</radius-modal>
|
||
|
||
<radius-modal :show="showPingModal" title="Ping Ergebnis" @close="showPingModal = false">
|
||
<div v-if="routerActionLoading && !pingResult" class="table-placeholder" style="height: 150px;">
|
||
<div class="btn-loader" style="width: 40px; height: 40px;"></div>
|
||
<div class="mt-3">Ping läuft...</div>
|
||
</div>
|
||
<div v-else-if="pingResult">
|
||
<div class="kv-redesign">
|
||
<div class="kv-row"><span class="kv-label">Gesendet</span><code class="kv-value">{{ pingResult.packetsTransmitted }}</code></div>
|
||
<div class="kv-row"><span class="kv-label">Empfangen</span><code class="kv-value">{{ pingResult.packetsReceived }}</code></div>
|
||
<div class="kv-row"><span class="kv-label">Verlust</span><code class="kv-value">{{ pingResult.packetLoss }}%</code></div>
|
||
<div class="kv-row"><span class="kv-label">Min / Avg / Max</span><code class="kv-value">{{ pingResult.min }} / {{ pingResult.avg }} / {{ pingResult.max }} ms</code></div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="table-placeholder" style="height: 150px;">
|
||
Kein Ergebnis.
|
||
</div>
|
||
</radius-modal>
|
||
|
||
<radius-modal :show="showSpeedtestModal" title="Speedtest Ergebnis" @close="showSpeedtestModal = false" modal-class="modal-card-wide">
|
||
<div v-if="speedtestLoading && speedtestHistory.length === 0" class="table-placeholder" style="height: 200px;">
|
||
<div class="btn-loader" style="width: 40px; height: 40px;"></div>
|
||
<div class="mt-3">Speedtest wird initialisiert...</div>
|
||
</div>
|
||
<div v-else>
|
||
<div class="table-wrap" style="max-height: 500px; overflow-y: auto;">
|
||
<table class="tt-table compact">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 60px;">#</th>
|
||
<th style="text-align: right">Bandbreite</th>
|
||
<th style="text-align: right">Übertragen</th>
|
||
<th style="text-align: right">Pakete</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(row, idx) in speedtestHistory" :key="idx">
|
||
<td class="mono small">{{ idx + 1 }}</td>
|
||
<td class="mono small" style="text-align: right">{{ row.bpsFormatted }}</td>
|
||
<td class="mono small" style="text-align: right">{{ row.bytesFormatted }}</td>
|
||
<td class="mono small" style="text-align: right">{{ row.packets }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div ref="speedtestBottom"></div>
|
||
</div>
|
||
<div v-if="speedtestLoading" class="center mt-3 muted small"><i class="fa-duotone fa-spinner fa-spin"></i> Aktualisiere...</div>
|
||
<div v-else class="center mt-3" style="color: var(--ok);"><i class="fa-duotone fa-check-circle"></i> Abgeschlossen</div>
|
||
</div>
|
||
</radius-modal>
|
||
|
||
<radius-modal :show="showRemoteAccessModal" title="Remote Zugriff Konfiguration" @close="showRemoteAccessModal = false">
|
||
<div v-if="remoteAccessLoading" class="table-placeholder" style="height: 200px;">
|
||
<div class="btn-loader" style="width: 40px; height: 40px;"></div>
|
||
<div class="mt-3">{{ remoteAccessStep }}</div>
|
||
</div>
|
||
<div v-else-if="remoteAccessResult">
|
||
<div class="alert ok mb-4" style="background-color: #eaf7ef; border: 1px solid #c9e6d8; color: #206a42; padding: 12px; border-radius: 8px;">
|
||
<i class="fa-duotone fa-check-circle"></i> Konfiguration erfolgreich abgeschlossen.
|
||
</div>
|
||
<div class="kv-redesign">
|
||
<div class="kv-row">
|
||
<span class="kv-label">Remote Link</span>
|
||
<div class="kv-value inline-copy">
|
||
<a :href="remoteAccessResult.link" target="_blank" class="link">{{ remoteAccessResult.link }}</a>
|
||
<button class="icon-btn sm" @click="copy(remoteAccessResult.link, $event)" data-tooltip="Kopieren">
|
||
<i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="kv-row">
|
||
<span class="kv-label">Username</span>
|
||
<div class="kv-value inline-copy">
|
||
<code class="mono">{{ remoteAccessResult.username }}</code>
|
||
<button class="icon-btn sm" @click="copy(remoteAccessResult.username, $event)" data-tooltip="Kopieren">
|
||
<i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div class="kv-row">
|
||
<span class="kv-label">Password</span>
|
||
<div class="kv-value inline-copy">
|
||
<code class="mono">{{ remoteAccessResult.password }}</code>
|
||
<button class="icon-btn sm" @click="copy(remoteAccessResult.password, $event)" data-tooltip="Kopieren">
|
||
<i class="fa-duotone fa-copy copy-icon"></i><i class="fa-duotone fa-check check-icon"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-else class="table-placeholder" style="height: 200px;">
|
||
Ein Fehler ist aufgetreten.
|
||
</div>
|
||
</radius-modal>
|
||
|
||
<radius-modal :show="showParameterModal" title="Parameter lesen" @close="showParameterModal = false">
|
||
<div>
|
||
<div class="field">
|
||
<label style="margin-bottom: 8px; font-size: 14px;">Parameter Name</label>
|
||
<div class="input-wrap">
|
||
<i class="fa-duotone fa-code input-icon"></i>
|
||
<input class="ri" type="text" v-model.trim="parameterName" placeholder="z.B. InternetGatewayDevice.User.1.Username">
|
||
</div>
|
||
<p class="muted small" style="margin-top: 4px;">Geben Sie den vollständigen Parameter-Pfad ein</p>
|
||
</div>
|
||
<div class="cluster" style="justify-content: flex-end; margin-top: 24px; gap: 12px;">
|
||
<button class="ghost-btn" @click="showParameterModal = false">Abbrechen</button>
|
||
<button class="primary-btn" @click="getParameter" :disabled="!parameterName || routerActionLoading">
|
||
<span v-if="!routerActionLoading">Lesen</span>
|
||
<span v-else class="btn-loader"></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</radius-modal>
|
||
|
||
<radius-modal :show="showSetParameterModal" title="Parameter setzen" @close="showSetParameterModal = false">
|
||
<div>
|
||
<div class="field">
|
||
<label style="margin-bottom: 8px; font-size: 14px;">Parameter Name</label>
|
||
<div class="input-wrap">
|
||
<i class="fa-duotone fa-code input-icon"></i>
|
||
<input class="ri" type="text" v-model.trim="setParameterName" placeholder="z.B. InternetGatewayDevice.User.1.Password">
|
||
</div>
|
||
</div>
|
||
<div class="field mt-2">
|
||
<label style="margin-bottom: 8px; font-size: 14px;">Wert</label>
|
||
<div class="input-wrap">
|
||
<i class="fa-duotone fa-input-text input-icon"></i>
|
||
<input class="ri" type="text" v-model.trim="setParameterValue" placeholder="Neuer Wert">
|
||
</div>
|
||
</div>
|
||
<div class="cluster" style="justify-content: flex-end; margin-top: 24px; gap: 12px;">
|
||
<button class="ghost-btn" @click="showSetParameterModal = false">Abbrechen</button>
|
||
<button class="primary-btn" @click="setParameter" :disabled="!setParameterName || !setParameterValue || routerActionLoading">
|
||
<span v-if="!routerActionLoading">Setzen</span>
|
||
<span v-else class="btn-loader"></span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</radius-modal>
|
||
</div>
|
||
`,
|
||
data: () => ({
|
||
window: window,
|
||
billAddrDisplay: '',
|
||
billAddrCustnum: '',
|
||
username: '',
|
||
ip: '',
|
||
info: '',
|
||
searchMode: 'autocomplete',
|
||
radiusUsers: [],
|
||
checkOnlineState: false,
|
||
isLoading: false,
|
||
showRadacctModal: false,
|
||
radacctData: null,
|
||
searchCount: 0,
|
||
hasSearched: false,
|
||
visibleCount: 50,
|
||
observer: null,
|
||
showTransferModal: false,
|
||
transferInitialLoading: false,
|
||
transferMonthlyLoading: false,
|
||
transferModalUsername: '',
|
||
transferYear: new Date().getFullYear(),
|
||
transferMonth: new Date().getMonth() + 1,
|
||
transferYearlyData: null,
|
||
transferMonthlyData: null,
|
||
transferChartInstance: null,
|
||
showYearDropdown: false,
|
||
isSendingEmail: false,
|
||
showEmailModal: false,
|
||
recipientEmail: '',
|
||
showExtensionIdModal: false,
|
||
extensionId: 'jglijfiddilckddlmbnlojmmlahboffh',
|
||
showRouterModal: false,
|
||
routerLoading: false,
|
||
routerActionLoading: false,
|
||
routerData: {},
|
||
routerDevice: null,
|
||
pingResult: null,
|
||
speedtestLoading: false,
|
||
speedtestResult: null,
|
||
speedtestHistory: [],
|
||
speedtestHasStarted: false,
|
||
showPingModal: false,
|
||
showSpeedtestModal: false,
|
||
showRemoteAccessModal: false,
|
||
remoteAccessLoading: false,
|
||
remoteAccessResult: null,
|
||
remoteAccessStep: '',
|
||
showParameterModal: false,
|
||
parameterName: '',
|
||
showSetParameterModal: false,
|
||
setParameterName: '',
|
||
setParameterValue: '',
|
||
managementUsername: '',
|
||
loadingUsername: false
|
||
}),
|
||
computed: {
|
||
hasFilters() {
|
||
return this.billAddrDisplay || this.username || this.ip || this.info;
|
||
},
|
||
visibleUsers() {
|
||
return this.radiusUsers.slice(0, this.visibleCount);
|
||
},
|
||
availableYears() {
|
||
const c = new Date().getFullYear(), s = 2021;
|
||
if (s > c) return [c];
|
||
return Array.from({length: c - s + 1}, (_, i) => c - i);
|
||
},
|
||
allMonths() {
|
||
return Array.from({length: 12}, (_, i) => ({
|
||
month: i + 1,
|
||
name: new Date(2000, i, 1).toLocaleString('de-DE', {month: 'short'})
|
||
}));
|
||
},
|
||
isValidEmail() {
|
||
if (!this.recipientEmail) return false;
|
||
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.recipientEmail);
|
||
}
|
||
},
|
||
mounted() {
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const infoParam = urlParams.get('info');
|
||
if (infoParam) {
|
||
this.info = infoParam;
|
||
this.loadRadiusUsers();
|
||
}
|
||
this.observer = new IntersectionObserver(([e]) => {
|
||
if (e && e.isIntersecting) this.loadMore();
|
||
}, {root: this.$refs.tableWrap, threshold: 0.1});
|
||
if (this.$refs.sentinel) this.observer.observe(this.$refs.sentinel);
|
||
|
||
const savedExtensionId = localStorage.getItem('radiusExtensionId');
|
||
if (savedExtensionId) {
|
||
this.extensionId = savedExtensionId;
|
||
}
|
||
window.addEventListener('keydown', this.handleKeydown);
|
||
},
|
||
beforeDestroy() {
|
||
if (this.observer) this.observer.disconnect();
|
||
if (this.transferChartInstance) this.transferChartInstance.destroy();
|
||
window.removeEventListener('keydown', this.handleKeydown);
|
||
},
|
||
updated() {
|
||
if (this.observer && this.$refs.sentinel) {
|
||
this.observer.disconnect();
|
||
this.observer.observe(this.$refs.sentinel);
|
||
}
|
||
},
|
||
methods: {
|
||
handleKeydown(e) {
|
||
console.log(e);
|
||
if (e.code === 'KeyE' && e.ctrlKey && e.altKey) {
|
||
e.preventDefault();
|
||
this.openExtensionIdModal();
|
||
}
|
||
},
|
||
openExtensionIdModal() {
|
||
this.showExtensionIdModal = true;
|
||
},
|
||
saveExtensionId() {
|
||
localStorage.setItem('radiusExtensionId', this.extensionId);
|
||
this.showExtensionIdModal = false;
|
||
window.notify('success', 'Extension ID gespeichert.');
|
||
},
|
||
onAddrSelect({custnum, display}) {
|
||
this.billAddrCustnum = custnum || '';
|
||
this.billAddrDisplay = display || '';
|
||
},
|
||
onModeChange(newMode) {
|
||
this.searchMode = newMode;
|
||
},
|
||
async loadRadiusUsers() {
|
||
this.isLoading = true;
|
||
this.radiusUsers = [];
|
||
this.hasSearched = true;
|
||
this.visibleCount = 50;
|
||
try {
|
||
const p = new URLSearchParams({
|
||
username: this.username || '',
|
||
info: this.info || '',
|
||
ip: this.ip || ''
|
||
});
|
||
if (this.searchMode === 'text') p.set('estmk_nr', this.billAddrDisplay || ''); else p.set('custnum', this.billAddrCustnum || '');
|
||
const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?${p.toString()}`);
|
||
if (r.ok) {
|
||
const u = await r.json();
|
||
if (Array.isArray(u) && u.length < 6) this.checkOnlineState = true;
|
||
this.radiusUsers = Array.isArray(u) ? u : [];
|
||
}
|
||
} catch (e) {
|
||
console.error(e);
|
||
}
|
||
this.isLoading = false;
|
||
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const connectParam = urlParams.get('connect');
|
||
if (connectParam && connectParam.toLowerCase() === 'true' && this.radiusUsers.length === 1) {
|
||
const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=fetchRadacct&username=${encodeURIComponent(this.radiusUsers[0].username)}`);
|
||
if (r.ok) {
|
||
const radacct = await r.json();
|
||
if (radacct && radacct.ip) {
|
||
this.onScanIp({ip: radacct.ip}, this.radiusUsers[0]);
|
||
}
|
||
}
|
||
}
|
||
|
||
this.searchCount++;
|
||
},
|
||
async fetchRadacctData(username) {
|
||
this.showRadacctModal = true;
|
||
this.radacctData = null;
|
||
try {
|
||
const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=fetchRadacct&username=${encodeURIComponent(username)}`);
|
||
if (r.ok) this.radacctData = await r.json();
|
||
} catch (e) {
|
||
console.error(e);
|
||
this.radacctData = {};
|
||
}
|
||
},
|
||
async copy(text, event) {
|
||
if (!event || !event.currentTarget) return;
|
||
const btn = event.currentTarget;
|
||
if (btn.classList.contains('is-copied')) return;
|
||
await window.RadiusUtils.copyToClipboard(text);
|
||
btn.classList.add('is-copied');
|
||
btn.disabled = true;
|
||
setTimeout(() => {
|
||
btn.classList.remove('is-copied');
|
||
btn.disabled = false;
|
||
}, 1500);
|
||
},
|
||
clearFilters() {
|
||
this.billAddrDisplay = '';
|
||
this.billAddrCustnum = '';
|
||
this.username = '';
|
||
this.ip = '';
|
||
this.info = '';
|
||
this.radiusUsers = [];
|
||
this.hasSearched = false;
|
||
this.searchCount++;
|
||
this.visibleCount = 50;
|
||
},
|
||
loadMore() {
|
||
if (this.visibleCount < this.radiusUsers.length) this.visibleCount += 50;
|
||
},
|
||
async onScanIp(payload, item) {
|
||
const {ip} = payload;
|
||
const info = item.info;
|
||
if (!ip) return;
|
||
|
||
window.notify('info', `Starte Scan für ${ip}...`);
|
||
|
||
try {
|
||
const response = await fetch(`http://localhost:8094/scan?ip=${ip}`);
|
||
|
||
if (!response.ok) {
|
||
window.notify('error', `Scan-Server-Fehler: ${response.status}`);
|
||
return;
|
||
}
|
||
|
||
const data = await response.json();
|
||
|
||
if (data.status === 'success' && data.url) {
|
||
const extensionId = this.extensionId;
|
||
const message = {
|
||
type: "INITIATE_ROUTER_LOGIN",
|
||
payload: {
|
||
ip: ip,
|
||
url: data.url,
|
||
info: info
|
||
}
|
||
};
|
||
|
||
if (window.chrome && chrome.runtime && chrome.runtime.sendMessage) {
|
||
try {
|
||
chrome.runtime.sendMessage(extensionId, message, (response) => {
|
||
if (chrome.runtime.lastError) {
|
||
console.warn("Senden an Erweiterung fehlgeschlagen:", chrome.runtime.lastError.message);
|
||
window.notify('warning', 'Scan-Daten konnten nicht an die Erweiterung gesendet werden. (Drücke STRG + ALT + E zum Konfigurieren)');
|
||
} else {
|
||
console.log("Erweiterung hat geantwortet:", response);
|
||
}
|
||
});
|
||
} catch (e) {
|
||
console.error("Fehler beim Senden an die Erweiterung:", e);
|
||
window.notify('error', 'Fehler beim Senden an die Erweiterung.');
|
||
}
|
||
} else {
|
||
console.warn("Chrome Extension Messaging API nicht verfügbar.");
|
||
window.notify('warning', 'Chrome Messaging API nicht gefunden.');
|
||
}
|
||
|
||
} else if (data.status === 'not_found') {
|
||
window.notify('warning', `Kein Gerät für ${ip} gefunden.`);
|
||
} else {
|
||
window.notify('error', 'Ungültige Antwort vom Scan-Server.');
|
||
}
|
||
} catch (error) {
|
||
console.error('IP-Scan fehlgeschlagen:', error);
|
||
window.notify('error', `Scan für ${ip} fehlgeschlagen.`);
|
||
}
|
||
},
|
||
async openTransferModal(username) {
|
||
this.showTransferModal = true;
|
||
this.transferModalUsername = username;
|
||
this.transferYear = new Date().getFullYear();
|
||
this.transferMonth = new Date().getMonth() + 1;
|
||
await this.fetchTransferYearData();
|
||
},
|
||
closeTransferModal() {
|
||
this.showTransferModal = false;
|
||
this.transferModalUsername = '';
|
||
this.transferYearlyData = null;
|
||
this.transferMonthlyData = null;
|
||
this.showYearDropdown = false;
|
||
this.showEmailModal = false;
|
||
this.recipientEmail = '';
|
||
this.isSendingEmail = false;
|
||
if (this.transferChartInstance) {
|
||
this.transferChartInstance.destroy();
|
||
this.transferChartInstance = null;
|
||
}
|
||
},
|
||
async fetchTransferYearData() {
|
||
this.transferInitialLoading = true;
|
||
this.transferYearlyData = null;
|
||
try {
|
||
const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=transferStatistic&username=${this.transferModalUsername}&year=${this.transferYear}&month=0`);
|
||
if (r.ok) {
|
||
const d = await r.json();
|
||
if (d && d.monthlySummary) {
|
||
this.transferYearlyData = d;
|
||
const last = [...d.monthlySummary].reverse().find(m => m.grandTotalBytes > 0);
|
||
this.transferMonth = last ? last.month : new Date().getMonth() + 1;
|
||
await this.fetchTransferMonthData();
|
||
}
|
||
} else this.transferYearlyData = null;
|
||
} catch (e) {
|
||
console.error(e);
|
||
this.transferYearlyData = null;
|
||
}
|
||
this.transferInitialLoading = false;
|
||
},
|
||
async fetchTransferMonthData() {
|
||
this.transferMonthlyLoading = true;
|
||
this.transferMonthlyData = null;
|
||
if (this.transferChartInstance) this.transferChartInstance.destroy();
|
||
try {
|
||
const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=transferStatistic&username=${this.transferModalUsername}&year=${this.transferYear}&month=${this.transferMonth}`);
|
||
this.transferMonthlyData = r.ok ? await r.json() : null;
|
||
} catch (e) {
|
||
console.error(e);
|
||
this.transferMonthlyData = null;
|
||
}
|
||
this.transferMonthlyLoading = false;
|
||
this.$nextTick(() => {
|
||
if (this.showTransferModal) this.renderTransferChart();
|
||
});
|
||
},
|
||
prepareEmailModal() {
|
||
if (this.transferInitialLoading || !this.transferYearlyData || !this.transferYearlyData.yearlySummary || this.transferYearlyData.yearlySummary.grandTotalBytes === 0) return;
|
||
this.recipientEmail = '';
|
||
this.showEmailModal = true;
|
||
},
|
||
async sendTransferEmail() {
|
||
if (!this.transferMonthlyData || !this.transferChartInstance || !this.isValidEmail) return;
|
||
this.isSendingEmail = true;
|
||
try {
|
||
const chartImageBase64 = this.transferChartInstance.toBase64Image();
|
||
const payload = {
|
||
username: this.transferModalUsername,
|
||
year: this.transferYear,
|
||
month: this.transferMonth,
|
||
monthlySummary: this.transferMonthlyData.summary,
|
||
monthlyDetails: this.transferMonthlyData.details,
|
||
chartImage: chartImageBase64,
|
||
recipient: this.recipientEmail
|
||
};
|
||
const res = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/sendCustomerEmail`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(payload)
|
||
});
|
||
if (res.ok) {
|
||
window.notify('success', 'E-Mail wurde erfolgreich versendet.');
|
||
this.showEmailModal = false;
|
||
} else {
|
||
throw new Error('Server responded with an error.');
|
||
}
|
||
} catch (e) {
|
||
console.error("Failed to send transfer email:", e);
|
||
window.notify('error', 'Fehler beim Senden der E-Mail.');
|
||
} finally {
|
||
this.isSendingEmail = false;
|
||
}
|
||
},
|
||
isMonthDisabled(month) {
|
||
if (this.transferInitialLoading || this.transferMonthlyLoading) return true;
|
||
if (!this.transferYearlyData?.monthlySummary) return true;
|
||
const m = this.transferYearlyData.monthlySummary.find(m => m.month === month);
|
||
return !m || m.grandTotalBytes === 0;
|
||
},
|
||
selectYear(year) {
|
||
this.showYearDropdown = false;
|
||
if (this.transferYear !== year) this.changeTransferYear(year);
|
||
},
|
||
async changeTransferYear(year) {
|
||
this.transferYear = year;
|
||
await this.fetchTransferYearData();
|
||
},
|
||
async changeTransferMonth(month) {
|
||
this.transferMonth = month;
|
||
await this.fetchTransferMonthData();
|
||
},
|
||
processChartData(details) {
|
||
if (!details || !details.length) return {labels: [], datasets: []};
|
||
const daily = details.reduce((a, s) => {
|
||
const d = s.startTime.split(' ')[0];
|
||
if (!a[d]) a[d] = {downloadBytes: 0, uploadBytes: 0};
|
||
a[d].downloadBytes += Number(s.downloadBytes) || 0;
|
||
a[d].uploadBytes += Number(s.uploadBytes) || 0;
|
||
return a;
|
||
}, {});
|
||
const dates = Object.keys(daily).sort((a, b) => new Date(a) - new Date(b));
|
||
return {
|
||
labels: dates,
|
||
datasets: [{
|
||
label: 'Download',
|
||
data: dates.map(d => daily[d].downloadBytes),
|
||
borderColor: 'rgba(15, 157, 88, 0.8)',
|
||
backgroundColor: 'rgba(15, 157, 88, 0.1)',
|
||
fill: true,
|
||
tension: 0.3,
|
||
pointRadius: 2,
|
||
borderWidth: 1.5
|
||
}, {
|
||
label: 'Upload',
|
||
data: dates.map(d => daily[d].uploadBytes),
|
||
borderColor: 'rgba(0, 83, 132, 0.8)',
|
||
backgroundColor: 'rgba(0, 83, 132, 0.1)',
|
||
fill: true,
|
||
tension: 0.3,
|
||
pointRadius: 2,
|
||
borderWidth: 1.5
|
||
}]
|
||
};
|
||
},
|
||
renderTransferChart() {
|
||
if (this.transferChartInstance) this.transferChartInstance.destroy();
|
||
if (!this.$refs.transferChartCanvas || !this.transferMonthlyData?.details?.length || !window.Chart) return;
|
||
const d = this.processChartData(this.transferMonthlyData.details);
|
||
if (!d.labels.length) return;
|
||
const chartBackgroundColorPlugin = {
|
||
id: 'customCanvasBackgroundColor', beforeDraw: (chart) => {
|
||
const {ctx} = chart;
|
||
ctx.save();
|
||
ctx.globalCompositeOperation = 'destination-over';
|
||
ctx.fillStyle = '#ffffff';
|
||
ctx.fillRect(0, 0, chart.width, chart.height);
|
||
ctx.restore();
|
||
}
|
||
};
|
||
this.transferChartInstance = new Chart(this.$refs.transferChartCanvas.getContext('2d'), {
|
||
type: 'line',
|
||
data: d,
|
||
options: {
|
||
responsive: true,
|
||
maintainAspectRatio: false,
|
||
scales: {
|
||
x: {
|
||
type: 'time',
|
||
time: {unit: 'day', tooltipFormat: 'DD.MM.YYYY', displayFormats: {day: 'DD.MM'}},
|
||
grid: {display: false},
|
||
ticks: {maxRotation: 0, autoSkip: true, maxTicksLimit: 15}
|
||
},
|
||
y: {
|
||
beginAtZero: true,
|
||
ticks: {callback: (v) => window.RadiusUtils.formatBytes(v, 0)},
|
||
grid: {color: 'rgba(0,0,0,0.05)'}
|
||
}
|
||
},
|
||
plugins: {
|
||
tooltip: {callbacks: {label: (c) => `${c.dataset.label || ''}: ${window.RadiusUtils.formatBytes(c.parsed.y)}`}},
|
||
legend: {position: 'bottom', labels: {usePointStyle: true, boxWidth: 8, padding: 20}}
|
||
},
|
||
interaction: {mode: 'index', intersect: false}
|
||
},
|
||
plugins: [chartBackgroundColorPlugin]
|
||
});
|
||
},
|
||
async openRouterManagement(item) {
|
||
this.showRouterModal = true;
|
||
this.routerLoading = true;
|
||
this.routerData = item;
|
||
this.routerDevice = null;
|
||
this.pingResult = null;
|
||
this.speedtestResult = null;
|
||
this.speedtestLoading = false;
|
||
this.managementUsername = '';
|
||
|
||
try {
|
||
// First get the IP from radacct
|
||
const r = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/proxyUnsecureHTTPRequestToRadius?action2=fetchRadacct&username=${encodeURIComponent(item.username)}`);
|
||
if (r.ok) {
|
||
const radacct = await r.json();
|
||
if (radacct && radacct.ip) {
|
||
// Now fetch device from GenieACS using this IP
|
||
const deviceResponse = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsGetDeviceByIp?ip=${encodeURIComponent(radacct.ip)}`);
|
||
if (deviceResponse.ok) {
|
||
const deviceData = await deviceResponse.json();
|
||
if (deviceData.success) {
|
||
this.routerDevice = deviceData;
|
||
// Management Username removed
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Error fetching router:', e);
|
||
window.notify('error', 'Fehler beim Laden des Routers');
|
||
}
|
||
|
||
this.routerLoading = false;
|
||
},
|
||
closeRouterModal() {
|
||
this.showRouterModal = false;
|
||
this.routerData = {};
|
||
this.routerDevice = null;
|
||
this.pingResult = null;
|
||
this.speedtestResult = null;
|
||
this.speedtestLoading = false;
|
||
this.showParameterModal = false;
|
||
this.showSetParameterModal = false;
|
||
this.parameterName = '';
|
||
this.setParameterName = '';
|
||
this.setParameterValue = '';
|
||
this.managementUsername = '';
|
||
this.loadingUsername = false;
|
||
},
|
||
async refreshRouter() {
|
||
if (!this.routerDevice || !this.routerDevice.deviceId) return;
|
||
|
||
this.routerActionLoading = true;
|
||
try {
|
||
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsRefreshDevice`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({deviceId: this.routerDevice.deviceId})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
window.notify('success', 'Router-Aktualisierung gestartet');
|
||
} else {
|
||
window.notify('error', data.message || 'Fehler beim Aktualisieren');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Error refreshing router:', e);
|
||
window.notify('error', 'Fehler beim Aktualisieren des Routers');
|
||
}
|
||
this.routerActionLoading = false;
|
||
},
|
||
async rebootRouter() {
|
||
if (!this.routerDevice || !this.routerDevice.deviceId) return;
|
||
if (!confirm('Möchten Sie den Router wirklich neu starten?')) return;
|
||
|
||
this.routerActionLoading = true;
|
||
try {
|
||
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsRebootDevice`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({deviceId: this.routerDevice.deviceId})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
window.notify('success', 'Router-Neustart gestartet');
|
||
} else {
|
||
window.notify('error', data.message || 'Fehler beim Neustart');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Error rebooting router:', e);
|
||
window.notify('error', 'Fehler beim Neustarten des Routers');
|
||
}
|
||
this.routerActionLoading = false;
|
||
},
|
||
async pingRouter() {
|
||
if (!this.routerDevice) return;
|
||
|
||
// Use management IP for ping, fallback to external IP
|
||
const pingIp = this.routerDevice.managementIp || this.routerDevice.ip;
|
||
if (!pingIp) return;
|
||
|
||
this.showPingModal = true; // Open modal immediately
|
||
this.routerActionLoading = true;
|
||
this.pingResult = null;
|
||
try {
|
||
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsPing?ip=${encodeURIComponent(pingIp)}`);
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success && data.result) {
|
||
this.pingResult = data.result;
|
||
window.notify('success', 'Ping erfolgreich');
|
||
} else {
|
||
window.notify('error', 'Ping fehlgeschlagen');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Error pinging router:', e);
|
||
window.notify('error', 'Fehler beim Pingen des Routers');
|
||
}
|
||
this.routerActionLoading = false;
|
||
},
|
||
async confirmFactoryReset() {
|
||
if (!confirm('ACHTUNG: Möchten Sie den Router wirklich auf Werkseinstellungen zurücksetzen? Diese Aktion kann nicht rückgängig gemacht werden!')) return;
|
||
if (!confirm('Sind Sie sicher? Alle Einstellungen gehen verloren!')) return;
|
||
|
||
await this.factoryResetRouter();
|
||
},
|
||
async factoryResetRouter() {
|
||
if (!this.routerDevice || !this.routerDevice.deviceId) return;
|
||
|
||
this.routerActionLoading = true;
|
||
try {
|
||
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsFactoryReset`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({deviceId: this.routerDevice.deviceId})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
window.notify('success', 'Factory Reset gestartet');
|
||
} else {
|
||
window.notify('error', data.message || 'Fehler beim Factory Reset');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Error factory resetting router:', e);
|
||
window.notify('error', 'Fehler beim Factory Reset');
|
||
}
|
||
this.routerActionLoading = false;
|
||
},
|
||
async getParameter() {
|
||
if (!this.routerDevice || !this.routerDevice.deviceId || !this.parameterName) return;
|
||
|
||
this.routerActionLoading = true;
|
||
try {
|
||
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsGetParameters`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
deviceId: this.routerDevice.deviceId,
|
||
parameters: [this.parameterName]
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const data = await response.json();
|
||
if (data.success) {
|
||
window.notify('success', 'Parameter-Abfrage gestartet. Überprüfen Sie das GenieACS-Interface für Ergebnisse.');
|
||
this.showParameterModal = false;
|
||
this.parameterName = '';
|
||
} else {
|
||
window.notify('error', data.message || 'Fehler beim Lesen des Parameters');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Error getting parameter:', e);
|
||
window.notify('error', 'Fehler beim Lesen des Parameters');
|
||
}
|
||
this.routerActionLoading = false;
|
||
},
|
||
async setParameterValues(parameters) {
|
||
if (!this.routerDevice || !this.routerDevice.deviceId || !parameters) return false;
|
||
try {
|
||
const response = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsSetParameters`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
deviceId: this.routerDevice.deviceId,
|
||
parameters: parameters
|
||
})
|
||
});
|
||
return response.ok && (await response.json()).success;
|
||
} catch (e) {
|
||
console.error('Error setting parameters:', e);
|
||
return false;
|
||
}
|
||
},
|
||
async setParameter() {
|
||
if (!this.routerDevice || !this.routerDevice.deviceId || !this.setParameterName || !this.setParameterValue) return;
|
||
|
||
this.routerActionLoading = true;
|
||
const parameters = {};
|
||
parameters[this.setParameterName] = this.setParameterValue;
|
||
|
||
if (await this.setParameterValues(parameters)) {
|
||
window.notify('success', 'Parameter erfolgreich gesetzt');
|
||
this.showSetParameterModal = false;
|
||
this.setParameterName = '';
|
||
this.setParameterValue = '';
|
||
} else {
|
||
window.notify('error', 'Fehler beim Setzen des Parameters');
|
||
}
|
||
this.routerActionLoading = false;
|
||
},
|
||
async runSpeedtest() {
|
||
if (!this.routerDevice || !this.routerDevice.deviceId) return;
|
||
this.showSpeedtestModal = true; // Open modal immediately
|
||
this.speedtestLoading = true;
|
||
this.speedtestResult = null;
|
||
this.speedtestHistory = [];
|
||
this.speedtestHasStarted = false;
|
||
|
||
try {
|
||
// 1. Set CPE Parameters
|
||
const params = {
|
||
'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.Start': 1,
|
||
'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.StartBidirect': 1,
|
||
'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.WANAccess': true
|
||
};
|
||
|
||
if (!await this.setParameterValues(params)) {
|
||
throw new Error("Konnte Speedtest-Parameter nicht setzen");
|
||
}
|
||
|
||
// 2. Trigger Speedtest Server
|
||
const ip = this.routerDevice.ip; // External IP
|
||
if (!ip) throw new Error("Keine IP-Adresse gefunden");
|
||
|
||
const stRes = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsRunSpeedtest`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({ ip: ip })
|
||
});
|
||
|
||
if (!stRes.ok) {
|
||
const err = await stRes.json();
|
||
throw new Error(err.message || "Speedtest-Server Fehler");
|
||
}
|
||
|
||
// 3. Poll for result
|
||
this.pollSpeedtestResult();
|
||
|
||
} catch (e) {
|
||
window.notify('error', e.message);
|
||
this.speedtestLoading = false;
|
||
}
|
||
},
|
||
async pollSpeedtestResult() {
|
||
let attempts = 0;
|
||
const maxAttempts = 240; // 2 min
|
||
const resultParam = 'InternetGatewayDevice.X_AVM-DE_SpeedtestServer.UDP.Result';
|
||
|
||
const poll = async () => {
|
||
if (!this.showSpeedtestModal) return;
|
||
if (attempts >= maxAttempts) {
|
||
this.speedtestLoading = false;
|
||
window.notify('error', 'Speedtest Zeitüberschreitung');
|
||
return;
|
||
}
|
||
attempts++;
|
||
|
||
try {
|
||
await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsGetParameters`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
deviceId: this.routerDevice.deviceId,
|
||
parameters: [resultParam]
|
||
})
|
||
});
|
||
|
||
setTimeout(async () => {
|
||
try {
|
||
const val = await this.fetchDeviceParameterValue(resultParam);
|
||
if (val && typeof val === 'string' && val.includes("BPS")) {
|
||
const parsed = this.parseSpeedtestResult(val);
|
||
if (parsed) {
|
||
this.speedtestHistory.push(parsed);
|
||
this.$nextTick(() => {
|
||
if (this.$refs.speedtestBottom) {
|
||
this.$refs.speedtestBottom.scrollIntoView({ behavior: 'smooth' });
|
||
}
|
||
});
|
||
|
||
if (parsed.bps > 0) this.speedtestHasStarted = true;
|
||
|
||
if (this.speedtestHasStarted && parsed.bps === 0) {
|
||
this.speedtestLoading = false;
|
||
window.notify('success', 'Speedtest abgeschlossen');
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
} catch(e) { console.error(e); }
|
||
|
||
if (this.speedtestLoading) setTimeout(poll, 500);
|
||
}, 500);
|
||
|
||
} catch (e) {
|
||
console.error(e);
|
||
if (this.speedtestLoading) setTimeout(poll, 500);
|
||
}
|
||
};
|
||
poll();
|
||
},
|
||
async fetchDeviceParameterValue(paramName) {
|
||
if (!this.routerDevice || !this.routerDevice.deviceId) return null;
|
||
try {
|
||
const deviceResponse = await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsGetDeviceInfo?deviceId=${encodeURIComponent(this.routerDevice.deviceId)}`);
|
||
if (deviceResponse.ok) {
|
||
const deviceInfo = await deviceResponse.json();
|
||
if (deviceInfo.success && deviceInfo.fullData) {
|
||
const paramData = deviceInfo.fullData[paramName];
|
||
if (paramData && paramData.value && paramData.value[0]) {
|
||
return paramData.value[0];
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Error fetching parameter value:', e);
|
||
}
|
||
return null;
|
||
},
|
||
formatBits(bps) {
|
||
if (!bps) return '0 Mbit/s';
|
||
// 1 Mbit = 1,000,000 bits (Standard network speed unit)
|
||
const mbits = bps / 1000000;
|
||
return mbits.toFixed(2) + ' Mbit/s';
|
||
},
|
||
parseSpeedtestResult(raw) {
|
||
try {
|
||
const bpsMatch = raw.match(/BPS\s+(\d+)/);
|
||
const bytesMatch = raw.match(/Bytes\s+(\d+)/);
|
||
const packetsMatch = raw.match(/Packets\s+(\d+)/);
|
||
|
||
if (bpsMatch) {
|
||
const bps = parseInt(bpsMatch[1]);
|
||
const bytes = bytesMatch ? parseInt(bytesMatch[1]) : 0;
|
||
const packets = packetsMatch ? parseInt(packetsMatch[1]) : 0;
|
||
|
||
return {
|
||
raw: raw,
|
||
bps: bps,
|
||
bpsFormatted: this.formatBits(bps),
|
||
bytes: bytes,
|
||
bytesFormatted: window.RadiusUtils.formatBytes(bytes),
|
||
packets: packets
|
||
};
|
||
}
|
||
} catch (e) {
|
||
console.error("Error parsing speedtest result", e);
|
||
}
|
||
return null;
|
||
},
|
||
generatePassword(length) {
|
||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||
let ret = "";
|
||
for (let i = 0; i < length; ++i) {
|
||
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||
}
|
||
return ret;
|
||
},
|
||
async runRemoteAccess() {
|
||
if (!this.routerDevice || !this.routerDevice.deviceId) return;
|
||
|
||
this.showRemoteAccessModal = true;
|
||
this.remoteAccessLoading = true;
|
||
this.remoteAccessStep = 'Konfiguriere Parameter...';
|
||
this.remoteAccessResult = null;
|
||
|
||
const password = this.generatePassword(12);
|
||
const timestamp = Math.floor(Date.now() / 1000).toString();
|
||
|
||
try {
|
||
const params = {
|
||
'InternetGatewayDevice.User.1.Enable': true,
|
||
'InternetGatewayDevice.User.1.Password': password,
|
||
'InternetGatewayDevice.User.1.RemoteAccessCapable': true,
|
||
'InternetGatewayDevice.User.1.Username': timestamp
|
||
};
|
||
|
||
const success = await this.setParameterValues(params);
|
||
if (!success) throw new Error("Fehler beim Setzen der Parameter");
|
||
|
||
this.remoteAccessStep = 'Warte auf TR069-User...';
|
||
this.pollRemoteUsername(password);
|
||
|
||
} catch (e) {
|
||
this.remoteAccessLoading = false;
|
||
window.notify('error', e.message);
|
||
}
|
||
},
|
||
async pollRemoteUsername(password) {
|
||
let attempts = 0;
|
||
const maxAttempts = 60; // 60 * 2s = 120s
|
||
const userParam = 'InternetGatewayDevice.User.1.Username';
|
||
|
||
const poll = async () => {
|
||
if (!this.showRemoteAccessModal) return;
|
||
if (attempts >= maxAttempts) {
|
||
this.remoteAccessLoading = false;
|
||
window.notify('error', 'Remote Access Zeitüberschreitung');
|
||
return;
|
||
}
|
||
attempts++;
|
||
|
||
try {
|
||
// Refresh param
|
||
await fetch(`${window.TT_CONFIG.BASE_PATH}/Radius/genieacsGetParameters`, {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify({
|
||
deviceId: this.routerDevice.deviceId,
|
||
parameters: [userParam]
|
||
})
|
||
});
|
||
|
||
setTimeout(async () => {
|
||
try {
|
||
const val = await this.fetchDeviceParameterValue(userParam);
|
||
if (val && typeof val === 'string' && val.startsWith('TR069-')) {
|
||
this.remoteAccessResult = {
|
||
link: `https://${this.routerDevice.ip}:9090`,
|
||
username: val,
|
||
password: password
|
||
};
|
||
this.remoteAccessLoading = false;
|
||
return;
|
||
}
|
||
} catch(e) { console.error(e); }
|
||
|
||
if (this.remoteAccessLoading) setTimeout(poll, 2000);
|
||
}, 2000);
|
||
|
||
} catch (e) {
|
||
console.error(e);
|
||
if (this.remoteAccessLoading) setTimeout(poll, 2000);
|
||
}
|
||
};
|
||
poll();
|
||
},
|
||
async fetchManagementUsername() {
|
||
// Kept as requested method, but removed call in openRouterManagement if no longer needed
|
||
// User asked to remove "row", usually implies logic removal too to save resources?
|
||
// But I will keep the method definition just in case, but I already removed the UI row.
|
||
// Since the user asked to remove the "Row", I removed the UI part.
|
||
// The automatic fetching was in openRouterManagement, I removed it there too.
|
||
}
|
||
}
|
||
});
|