|
@@ -7,6 +7,37 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!-- Device Registration Toggle -->
|
|
|
|
|
+ <div class="registration-banner" :class="{ enabled: autoRegistrationEnabled }">
|
|
|
|
|
+ <div class="registration-content">
|
|
|
|
|
+ <div class="registration-info">
|
|
|
|
|
+ <h2>
|
|
|
|
|
+ <span v-if="autoRegistrationEnabled">🟢 {{ $t('devices.registrationEnabled') }}</span>
|
|
|
|
|
+ <span v-else>🔴 {{ $t('devices.registrationDisabled') }}</span>
|
|
|
|
|
+ </h2>
|
|
|
|
|
+ <p v-if="autoRegistrationEnabled" class="warning">
|
|
|
|
|
+ ⚠️ {{ $t('devices.registrationWarning') }}
|
|
|
|
|
+ <span v-if="registrationTimeLeft > 0">
|
|
|
|
|
+ - {{ $t('devices.autoDisableIn') }} {{ formatTimeLeft(registrationTimeLeft) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p v-else>{{ $t('devices.registrationHint') }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="registration-toggle">
|
|
|
|
|
+ <label class="toggle-switch-large">
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="checkbox"
|
|
|
|
|
+ :checked="autoRegistrationEnabled"
|
|
|
|
|
+ @change="toggleAutoRegistration"
|
|
|
|
|
+ :disabled="togglingRegistration"
|
|
|
|
|
+ />
|
|
|
|
|
+ <span class="toggle-slider"></span>
|
|
|
|
|
+ <span v-if="togglingRegistration" class="spinner">⏳</span>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<div class="content">
|
|
<div class="content">
|
|
|
<!-- Search and Filters -->
|
|
<!-- Search and Filters -->
|
|
|
<div class="filters-row">
|
|
<div class="filters-row">
|
|
@@ -437,6 +468,13 @@ import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
|
|
import devicesApi from '@/api/devices'
|
|
import devicesApi from '@/api/devices'
|
|
|
import organizationsApi from '@/api/organizations'
|
|
import organizationsApi from '@/api/organizations'
|
|
|
import tunnelsApi from '@/api/tunnels'
|
|
import tunnelsApi from '@/api/tunnels'
|
|
|
|
|
+import settingsApi from '@/api/settings'
|
|
|
|
|
+
|
|
|
|
|
+// Auto-registration state
|
|
|
|
|
+const autoRegistrationEnabled = ref(false)
|
|
|
|
|
+const togglingRegistration = ref(false)
|
|
|
|
|
+const registrationTimeLeft = ref(0)
|
|
|
|
|
+let registrationTimer = null
|
|
|
|
|
|
|
|
const devices = ref([])
|
|
const devices = ref([])
|
|
|
const organizations = ref([])
|
|
const organizations = ref([])
|
|
@@ -980,9 +1018,65 @@ async function saveDefaultConfig() {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// Auto-registration functions
|
|
|
|
|
+async function loadAutoRegistrationStatus() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const status = await settingsApi.getAutoRegistrationStatus()
|
|
|
|
|
+ autoRegistrationEnabled.value = status.enabled
|
|
|
|
|
+ registrationTimeLeft.value = status.time_left || 0
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Failed to load registration status:', err)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function toggleAutoRegistration() {
|
|
|
|
|
+ togglingRegistration.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const newState = !autoRegistrationEnabled.value
|
|
|
|
|
+ await settingsApi.toggleAutoRegistration(newState)
|
|
|
|
|
+ autoRegistrationEnabled.value = newState
|
|
|
|
|
+ if (newState) {
|
|
|
|
|
+ startRegistrationTimer()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ stopRegistrationTimer()
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ console.error('Failed to toggle registration:', err)
|
|
|
|
|
+ // Reload status to sync
|
|
|
|
|
+ await loadAutoRegistrationStatus()
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ togglingRegistration.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function startRegistrationTimer() {
|
|
|
|
|
+ stopRegistrationTimer()
|
|
|
|
|
+ registrationTimer = setInterval(async () => {
|
|
|
|
|
+ await loadAutoRegistrationStatus()
|
|
|
|
|
+ if (!autoRegistrationEnabled.value) {
|
|
|
|
|
+ stopRegistrationTimer()
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 10000) // Update every 10 seconds
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function stopRegistrationTimer() {
|
|
|
|
|
+ if (registrationTimer) {
|
|
|
|
|
+ clearInterval(registrationTimer)
|
|
|
|
|
+ registrationTimer = null
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function formatTimeLeft(seconds) {
|
|
|
|
|
+ if (seconds < 60) return `${seconds}s`
|
|
|
|
|
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`
|
|
|
|
|
+ return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
loadDevices()
|
|
loadDevices()
|
|
|
loadOrganizations()
|
|
loadOrganizations()
|
|
|
|
|
+ loadAutoRegistrationStatus()
|
|
|
|
|
+ startRegistrationTimer()
|
|
|
|
|
|
|
|
// Real-time polling every 10 seconds
|
|
// Real-time polling every 10 seconds
|
|
|
pollingInterval = setInterval(() => {
|
|
pollingInterval = setInterval(() => {
|
|
@@ -999,6 +1093,7 @@ onBeforeUnmount(() => {
|
|
|
if (searchDebounceTimer) {
|
|
if (searchDebounceTimer) {
|
|
|
clearTimeout(searchDebounceTimer)
|
|
clearTimeout(searchDebounceTimer)
|
|
|
}
|
|
}
|
|
|
|
|
+ stopRegistrationTimer()
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
@@ -1007,6 +1102,101 @@ onBeforeUnmount(() => {
|
|
|
.page-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 32px; }
|
|
.page-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 32px; }
|
|
|
.page-header h1 { font-size: 32px; font-weight: 700; color: #1a202c; margin-bottom: 8px; }
|
|
.page-header h1 { font-size: 32px; font-weight: 700; color: #1a202c; margin-bottom: 8px; }
|
|
|
.page-header p { color: #718096; font-size: 16px; }
|
|
.page-header p { color: #718096; font-size: 16px; }
|
|
|
|
|
+
|
|
|
|
|
+/* Registration Banner */
|
|
|
|
|
+.registration-banner {
|
|
|
|
|
+ background: #fff5f5;
|
|
|
|
|
+ border: 2px solid #fc8181;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ padding: 20px 24px;
|
|
|
|
|
+ margin-bottom: 24px;
|
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
|
+}
|
|
|
|
|
+.registration-banner.enabled {
|
|
|
|
|
+ background: #f0fff4;
|
|
|
|
|
+ border-color: #68d391;
|
|
|
|
|
+}
|
|
|
|
|
+.registration-content {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 24px;
|
|
|
|
|
+}
|
|
|
|
|
+.registration-info h2 {
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ color: #1a202c;
|
|
|
|
|
+}
|
|
|
|
|
+.registration-info p {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #718096;
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.registration-info .warning {
|
|
|
|
|
+ color: #c53030;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+.registration-banner.enabled .warning {
|
|
|
|
|
+ color: #276749;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* Large Toggle Switch */
|
|
|
|
|
+.toggle-switch-large {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ width: 80px;
|
|
|
|
|
+ height: 44px;
|
|
|
|
|
+}
|
|
|
|
|
+.toggle-switch-large input {
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ width: 0;
|
|
|
|
|
+ height: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.toggle-slider {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ bottom: 0;
|
|
|
|
|
+ background-color: #cbd5e0;
|
|
|
|
|
+ transition: 0.4s;
|
|
|
|
|
+ border-radius: 44px;
|
|
|
|
|
+ border: 2px solid #a0aec0;
|
|
|
|
|
+}
|
|
|
|
|
+.toggle-slider:before {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ height: 36px;
|
|
|
|
|
+ width: 36px;
|
|
|
|
|
+ left: 2px;
|
|
|
|
|
+ bottom: 2px;
|
|
|
|
|
+ background-color: white;
|
|
|
|
|
+ transition: 0.4s;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
|
|
|
+}
|
|
|
|
|
+.toggle-switch-large input:checked + .toggle-slider {
|
|
|
|
|
+ background-color: #48bb78;
|
|
|
|
|
+ border-color: #38a169;
|
|
|
|
|
+}
|
|
|
|
|
+.toggle-switch-large input:checked + .toggle-slider:before {
|
|
|
|
|
+ transform: translateX(36px);
|
|
|
|
|
+}
|
|
|
|
|
+.toggle-switch-large input:disabled + .toggle-slider {
|
|
|
|
|
+ opacity: 0.6;
|
|
|
|
|
+ cursor: not-allowed;
|
|
|
|
|
+}
|
|
|
|
|
+.toggle-switch-large .spinner {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.content { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); }
|
|
.content { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); }
|
|
|
|
|
|
|
|
/* Filters Row */
|
|
/* Filters Row */
|