Files
thetool/public/js/pages/Radius/RadiusUsers.js
2025-12-03 14:26:46 +00:00

1529 lines
78 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
}
}
});