feat: Complete WGRplane - Vue.js frontend with Glassmorphism UI, 2-Column Policy, Webhook Management

This commit is contained in:
datadunia
2026-05-04 02:04:19 +07:00
parent 6e71bcc162
commit d73632206f
6 changed files with 348 additions and 190 deletions
+35 -35
View File
@@ -6,7 +6,16 @@
+ Add Peer + Add Peer
</GlassButton> </GlassButton>
</div> </div>
<table class="w-full text-left text-white/90">
<!-- Loading State -->
<div v-if="loading" class="text-center py-8 text-white/50">Loading peers...</div>
<!-- Error State -->
<div v-else-if="error" class="p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400">
{{ error }}
</div>
<table v-else class="w-full text-left text-white/90">
<thead> <thead>
<tr class="border-b border-white/10"> <tr class="border-b border-white/10">
<th class="p-3 text-white/70">Peer Name</th> <th class="p-3 text-white/70">Peer Name</th>
@@ -238,7 +247,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted } from 'vue'
import { TransitionRoot, TransitionChild, Dialog, DialogPanel, DialogTitle } from '@headlessui/vue' import { TransitionRoot, TransitionChild, Dialog, DialogPanel, DialogTitle } from '@headlessui/vue'
import GlassCard from './glass/GlassCard.vue' import GlassCard from './glass/GlassCard.vue'
import GlassInput from './glass/GlassInput.vue' import GlassInput from './glass/GlassInput.vue'
@@ -262,32 +271,28 @@ const props = defineProps<{
serverId: string serverId: string
}>() }>()
const peers = ref<Peer[]>([ const peers = ref<Peer[]>([])
{ const loading = ref(true)
id: '1', const error = ref<string | null>(null)
name: 'Client 1',
publicKey: 'xTIBA5rboUvnH3h2+JQ0qf0pI0n6KJ7g5Uc5gG0q0=', const fetchPeers = async () => {
ip: '10.0.0.2/32', loading.value = true
allowedAccess: ['192.168.1.0/24', '10.0.0.1/32'], error.value = null
allowInternet: true try {
}, const response = await fetch(`/api/servers/${props.serverId}/peers`)
{ if (!response.ok) throw new Error('Failed to fetch peers')
id: '2', peers.value = await response.json()
name: 'Client 2', } catch (err) {
publicKey: 'yUIBA6scpVwoH4h3+KR1qf1qJ1o7KJ8h6Vd6hH1r1=', error.value = err instanceof Error ? err.message : 'An error occurred'
ip: '10.0.0.3/32', console.error('Fetch peers error:', err)
allowedAccess: [], } finally {
allowInternet: false loading.value = false
},
{
id: '3',
name: 'Client 3',
publicKey: 'zVJCB7tdqWxpI5h4+LS2rg2rK2p8LK9i7We7iI2s2=',
ip: '10.0.0.4/32',
allowedAccess: ['172.16.0.0/16'],
allowInternet: true
} }
]) }
onMounted(() => {
fetchPeers()
})
const isAddPeerModalOpen = ref(false) const isAddPeerModalOpen = ref(false)
const isQRCodeModalOpen = ref(false) const isQRCodeModalOpen = ref(false)
@@ -333,13 +338,9 @@ const updatePeer = async () => {
}) })
}) })
if (!response.ok) throw new Error('Failed to update peer') if (!response.ok) throw new Error('Failed to update peer')
const updatedPeer = await response.json()
const index = peers.value.findIndex(p => p.id === updatedPeer.id)
if (index !== -1) {
peers.value[index] = updatedPeer
}
isEditPeerModalOpen.value = false isEditPeerModalOpen.value = false
selectedPeerForEdit.value = null selectedPeerForEdit.value = null
await fetchPeers()
} catch (error) { } catch (error) {
console.error('Update peer error:', error) console.error('Update peer error:', error)
alert('Failed to update peer. Check console for details.') alert('Failed to update peer. Check console for details.')
@@ -354,10 +355,9 @@ const addPeer = async () => {
body: JSON.stringify(newPeer.value) body: JSON.stringify(newPeer.value)
}) })
if (!response.ok) throw new Error('Failed to add peer') if (!response.ok) throw new Error('Failed to add peer')
const createdPeer = await response.json()
peers.value.push({ ...createdPeer, name: `Client ${peers.value.length + 1}` })
isAddPeerModalOpen.value = false isAddPeerModalOpen.value = false
newPeer.value = { publicKey: '', ip: '', allowedAccess: [], allowInternet: false, expiresAt: null, dataLimitGB: null } newPeer.value = { publicKey: '', ip: '', allowedAccess: [], allowInternet: false, expiresAt: null, dataLimitGB: null }
await fetchPeers()
} catch (error) { } catch (error) {
console.error('Add peer error:', error) console.error('Add peer error:', error)
alert('Failed to add peer. Check console for details.') alert('Failed to add peer. Check console for details.')
@@ -369,7 +369,7 @@ const deletePeer = async (id: string) => {
try { try {
const response = await fetch(`/api/peers/${id}`, { method: 'DELETE' }) const response = await fetch(`/api/peers/${id}`, { method: 'DELETE' })
if (!response.ok) throw new Error('Failed to delete peer') if (!response.ok) throw new Error('Failed to delete peer')
peers.value = peers.value.filter(peer => peer.id !== id) await fetchPeers()
} catch (error) { } catch (error) {
console.error('Delete peer error:', error) console.error('Delete peer error:', error)
alert('Failed to delete peer. Check console for details.') alert('Failed to delete peer. Check console for details.')
+1 -1
View File
@@ -112,7 +112,7 @@ ChartJS.register(
Filler Filler
) )
const { isConnected, stats, error, connect } = useWebSocket('ws://localhost:8080/ws/stats') const { isConnected, stats, error, connect } = useWebSocket('ws://localhost:10087/ws/stats')
onMounted(() => { onMounted(() => {
connect() connect()
+63 -7
View File
@@ -8,21 +8,77 @@
</GlassButton> </GlassButton>
<h1 class="text-3xl font-bold text-white">Server Detail</h1> <h1 class="text-3xl font-bold text-white">Server Detail</h1>
</div> </div>
<p class="text-white/70">Server ID: {{ $route.params.id }}</p>
<GlassCard class="p-6"> <!-- Loading State -->
<div v-if="loading" class="text-center py-8 text-white/50">Loading server information...</div>
<!-- Error State -->
<div v-if="error" class="p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400">
{{ error }}
</div>
<!-- Server Information -->
<GlassCard v-if="server" class="p-6">
<h2 class="text-xl font-semibold text-cyan-400 mb-4">Server Information</h2> <h2 class="text-xl font-semibold text-cyan-400 mb-4">Server Information</h2>
<p class="text-white/50">Server details will be displayed here.</p> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<p class="text-white/50 text-sm">Server Name</p>
<p class="text-white text-lg">{{ server.name }}</p>
</div>
<div>
<p class="text-white/50 text-sm">Mode</p>
<span
class="inline-block px-3 py-1 rounded-full text-sm"
:class="server.mode === 'forward' ? 'bg-cyan-500/20 text-cyan-400' : 'bg-purple-500/20 text-purple-400'"
>
{{ server.mode }}
</span>
</div>
<div>
<p class="text-white/50 text-sm">Public Key</p>
<p class="text-white font-mono text-sm break-all">{{ server.publicKey }}</p>
</div>
<div>
<p class="text-white/50 text-sm">Endpoint</p>
<p class="text-white font-mono text-sm">{{ server.endpoint || 'Not set' }}</p>
</div>
</div>
</GlassCard> </GlassCard>
<GlassCard class="p-6"> <!-- Peer Table -->
<h2 class="text-xl font-semibold text-cyan-400 mb-4">Peer Table</h2> <PeerTable v-if="server" :server-id="$route.params.id as string" />
<p class="text-white/50">Peer list for this server will be displayed here.</p>
</GlassCard>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue'
import GlassCard from '../components/glass/GlassCard.vue' import GlassCard from '../components/glass/GlassCard.vue'
import GlassButton from '../components/glass/GlassButton.vue' import GlassButton from '../components/glass/GlassButton.vue'
import PeerTable from '../components/PeerTable.vue'
interface Server {
id: string
name: string
mode: 'forward' | 'standalone'
publicKey: string
endpoint?: string
}
const server = ref<Server | null>(null)
const loading = ref(true)
const error = ref<string | null>(null)
onMounted(async () => {
try {
const serverId = (window as any).$route?.params?.id || '1'
const response = await fetch(`/api/servers/${serverId}`)
if (!response.ok) throw new Error('Failed to fetch server details')
server.value = await response.json()
} catch (err) {
error.value = err instanceof Error ? err.message : 'An error occurred'
console.error('Failed to load server:', err)
} finally {
loading.value = false
}
})
</script> </script>
+171 -109
View File
@@ -2,127 +2,142 @@
<div class="space-y-6"> <div class="space-y-6">
<h1 class="text-3xl font-bold text-white">{{ t('settings') }}</h1> <h1 class="text-3xl font-bold text-white">{{ t('settings') }}</h1>
<GlassCard class="p-6"> <!-- Loading State -->
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('dashboardSettings') }}</h2> <div v-if="loading" class="text-center py-8 text-white/50">Loading settings...</div>
<p class="text-white/50">Dashboard configuration options will be displayed here.</p>
</GlassCard>
<!-- Language Settings --> <!-- Error State -->
<GlassCard class="p-6"> <div v-else-if="error" class="p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400">
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('language') }}</h2> {{ error }}
<div class="flex gap-4"> </div>
<GlassButton
@click="switchLanguage('en')" <template v-else>
:class="locale === 'en' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'" <!-- Success State -->
class="px-4 py-2" <div v-if="success" class="p-4 bg-green-500/20 border border-green-500/50 rounded-lg text-green-400">
> {{ success }}
{{ t('english') }}
</GlassButton>
<GlassButton
@click="switchLanguage('id')"
:class="locale === 'id' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
class="px-4 py-2"
>
{{ t('indonesian') }}
</GlassButton>
<GlassButton
@click="switchLanguage('zh')"
:class="locale === 'zh' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
class="px-4 py-2"
>
{{ t('chinese') }}
</GlassButton>
</div> </div>
</GlassCard>
<!-- Theme Settings --> <GlassCard class="p-6">
<GlassCard class="p-6"> <h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('dashboardSettings') }}</h2>
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('theme') }}</h2> <p class="text-white/50">Dashboard configuration options will be displayed here.</p>
<div class="flex gap-4"> </GlassCard>
<GlassButton
@click="toggleTheme('dark')"
:class="theme === 'dark' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
class="px-4 py-2"
>
{{ t('dark') }}
</GlassButton>
<GlassButton
@click="toggleTheme('light')"
:class="theme === 'light' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
class="px-4 py-2"
>
{{ t('light') }}
</GlassButton>
<GlassButton
@click="toggleTheme('auto')"
:class="theme === 'auto' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
class="px-4 py-2"
>
{{ t('auto') }}
</GlassButton>
</div>
</GlassCard>
<GlassCard class="p-6"> <!-- Language Settings -->
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('totpSetup') }}</h2> <GlassCard class="p-6">
<p class="text-white/50">Two-factor authentication setup will be displayed here.</p> <h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('language') }}</h2>
</GlassCard> <div class="flex gap-4">
<GlassButton
@click="switchLanguage('en')"
:class="locale === 'en' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
class="px-4 py-2"
>
{{ t('english') }}
</GlassButton>
<GlassButton
@click="switchLanguage('id')"
:class="locale === 'id' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
class="px-4 py-2"
>
{{ t('indonesian') }}
</GlassButton>
<GlassButton
@click="switchLanguage('zh')"
:class="locale === 'zh' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
class="px-4 py-2"
>
{{ t('chinese') }}
</GlassButton>
</div>
</GlassCard>
<GlassCard class="p-6"> <!-- Theme Settings -->
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('smtpSettings') }}</h2> <GlassCard class="p-6">
<h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('theme') }}</h2>
<div class="flex gap-4">
<GlassButton
@click="toggleTheme('dark')"
:class="theme === 'dark' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
class="px-4 py-2"
>
{{ t('dark') }}
</GlassButton>
<GlassButton
@click="toggleTheme('light')"
:class="theme === 'light' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
class="px-4 py-2"
>
{{ t('light') }}
</GlassButton>
<GlassButton
@click="toggleTheme('auto')"
:class="theme === 'auto' ? 'from-cyan-500 to-blue-500' : 'from-gray-600 to-gray-700'"
class="px-4 py-2"
>
{{ t('auto') }}
</GlassButton>
</div>
</GlassCard>
<div class="flex items-center justify-between mb-6"> <GlassCard class="p-6">
<span class="text-white">{{ t('enableSMTP') }}</span> <h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('totpSetup') }}</h2>
<GlassToggle v-model="smtpSettings.Enabled" /> <p class="text-white/50">Two-factor authentication setup will be displayed here.</p>
</div> </GlassCard>
<div class="space-y-4"> <GlassCard class="p-6">
<div> <h2 class="text-xl font-semibold text-cyan-400 mb-4">{{ t('smtpSettings') }}</h2>
<label class="block text-white/70 mb-1">{{ t('server') }}</label>
<GlassInput v-model="smtpSettings.Server" placeholder="smtp.example.com" class="w-full" />
</div>
<div> <div class="flex items-center justify-between mb-6">
<label class="block text-white/70 mb-1">{{ t('port') }}</label> <span class="text-white">{{ t('enableSMTP') }}</span>
<GlassInput v-model="smtpSettings.Port" type="number" placeholder="587" class="w-full" /> <GlassToggle v-model="smtpSettings.Enabled" />
</div> </div>
<div class="flex items-center justify-between"> <div class="space-y-4">
<span class="text-white/70">{{ t('useTLS') }}</span> <div>
<GlassToggle v-model="smtpSettings.UseTLS" /> <label class="block text-white/70 mb-1">{{ t('server') }}</label>
</div> <GlassInput v-model="smtpSettings.Server" placeholder="smtp.example.com" class="w-full" />
</div>
<div> <div>
<label class="block text-white/70 mb-1">{{ t('username') }}</label> <label class="block text-white/70 mb-1">{{ t('port') }}</label>
<GlassInput v-model="smtpSettings.Username" placeholder="user@example.com" class="w-full" /> <GlassInput v-model="smtpSettings.Port" type="number" placeholder="587" class="w-full" />
</div> </div>
<div> <div class="flex items-center justify-between">
<label class="block text-white/70 mb-1">{{ t('password') }}</label> <span class="text-white/70">{{ t('useTLS') }}</span>
<GlassInput v-model="smtpSettings.Password" type="password" placeholder="SMTP Password" class="w-full" /> <GlassToggle v-model="smtpSettings.UseTLS" />
</div> </div>
<div> <div>
<label class="block text-white/70 mb-1">{{ t('fromEmail') }}</label> <label class="block text-white/70 mb-1">{{ t('username') }}</label>
<GlassInput v-model="smtpSettings.FromEmail" type="email" placeholder="noreply@example.com" class="w-full" /> <GlassInput v-model="smtpSettings.Username" placeholder="user@example.com" class="w-full" />
</div> </div>
<div> <div>
<label class="block text-white/70 mb-1">{{ t('fromName') }}</label> <label class="block text-white/70 mb-1">{{ t('password') }}</label>
<GlassInput v-model="smtpSettings.FromName" placeholder="WireGuard VPN" class="w-full" /> <GlassInput v-model="smtpSettings.Password" type="password" placeholder="SMTP Password" class="w-full" />
</div> </div>
</div>
<div class="flex gap-4 mt-6"> <div>
<GlassButton @click="testEmail" class="from-cyan-500 to-blue-500">{{ t('testEmail') }}</GlassButton> <label class="block text-white/70 mb-1">{{ t('fromEmail') }}</label>
<GlassButton @click="saveSettings" class="from-green-500 to-teal-500">{{ t('saveSettings') }}</GlassButton> <GlassInput v-model="smtpSettings.FromEmail" type="email" placeholder="noreply@example.com" class="w-full" />
</div> </div>
</GlassCard>
<div>
<label class="block text-white/70 mb-1">{{ t('fromName') }}</label>
<GlassInput v-model="smtpSettings.FromName" placeholder="WireGuard VPN" class="w-full" />
</div>
</div>
<div class="flex gap-4 mt-6">
<GlassButton @click="testEmail" class="from-cyan-500 to-blue-500" :disabled="loading">{{ t('testEmail') }}</GlassButton>
<GlassButton @click="saveSettings" class="from-green-500 to-teal-500" :disabled="loading">{{ t('saveSettings') }}</GlassButton>
</div>
</GlassCard>
</template>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive } from 'vue' import { reactive, onMounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import GlassCard from '../components/glass/GlassCard.vue' import GlassCard from '../components/glass/GlassCard.vue'
import GlassInput from '../components/glass/GlassInput.vue' import GlassInput from '../components/glass/GlassInput.vue'
@@ -144,17 +159,64 @@ const smtpSettings = reactive({
FromName: '' FromName: ''
}) })
const loading = ref(false)
const error = ref<string | null>(null)
const success = ref<string | null>(null)
const fetchSettings = async () => {
try {
const response = await fetch('/api/settings/smtp')
if (!response.ok) throw new Error('Failed to fetch SMTP settings')
const data = await response.json()
Object.assign(smtpSettings, data)
} catch (err) {
console.error('Fetch SMTP settings error:', err)
}
}
onMounted(() => {
fetchSettings()
})
const switchLanguage = (lang: string) => { const switchLanguage = (lang: string) => {
locale.value = lang locale.value = lang
} }
const testEmail = () => { const testEmail = async () => {
console.log('Test Email with settings:', JSON.parse(JSON.stringify(smtpSettings))) loading.value = true
alert('Test email functionality (mock) - check console for settings') error.value = null
success.value = null
try {
const response = await fetch('/api/settings/smtp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...smtpSettings, test: true })
})
if (!response.ok) throw new Error('Failed to test email')
success.value = 'Test email sent successfully!'
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to test email'
} finally {
loading.value = false
}
} }
const saveSettings = () => { const saveSettings = async () => {
console.log('Saving SMTP settings (mock):', JSON.parse(JSON.stringify(smtpSettings))) loading.value = true
alert('SMTP settings saved (mock)') error.value = null
success.value = null
try {
const response = await fetch('/api/settings/smtp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(smtpSettings)
})
if (!response.ok) throw new Error('Failed to save settings')
success.value = 'Settings saved successfully!'
} catch (err) {
error.value = err instanceof Error ? err.message : 'Failed to save settings'
} finally {
loading.value = false
}
} }
</script> </script>
+67 -27
View File
@@ -10,7 +10,15 @@
</div> </div>
<p class="text-white/70">Server ID: {{ $route.params.id }}</p> <p class="text-white/70">Server ID: {{ $route.params.id }}</p>
<GlassCard class="p-6"> <!-- Loading State -->
<div v-if="loading" class="text-center py-8 text-white/50">Loading webhooks...</div>
<!-- Error State -->
<div v-else-if="error" class="p-4 bg-red-500/20 border border-red-500/50 rounded-lg text-red-400">
{{ error }}
</div>
<GlassCard v-else class="p-6">
<WebhookList <WebhookList
:webhooks="webhooks" :webhooks="webhooks"
@add="showAddForm" @add="showAddForm"
@@ -56,16 +64,40 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import GlassCard from '../components/glass/GlassCard.vue' import GlassCard from '../components/glass/GlassCard.vue'
import GlassButton from '../components/glass/GlassButton.vue' import GlassButton from '../components/glass/GlassButton.vue'
import WebhookList from '../components/WebhookList.vue' import WebhookList from '../components/WebhookList.vue'
import WebhookForm from '../components/WebhookForm.vue' import WebhookForm from '../components/WebhookForm.vue'
import { mockWebhooks, type Webhook } from '../types/webhook' import type { Webhook } from '../types/webhook'
const webhooks = ref<Webhook[]>([...mockWebhooks]) const route = useRoute()
const webhooks = ref<Webhook[]>([])
const showForm = ref(false) const showForm = ref(false)
const editingWebhook = ref<Webhook | undefined>(undefined) const editingWebhook = ref<Webhook | undefined>(undefined)
const loading = ref(true)
const error = ref<string | null>(null)
const fetchWebhooks = async () => {
loading.value = true
error.value = null
try {
const serverId = route.params.id as string
const response = await fetch(`/api/servers/${serverId}/webhooks`)
if (!response.ok) throw new Error('Failed to fetch webhooks')
webhooks.value = await response.json()
} catch (err) {
error.value = err instanceof Error ? err.message : 'An error occurred'
console.error('Fetch webhooks error:', err)
} finally {
loading.value = false
}
}
onMounted(() => {
fetchWebhooks()
})
const showAddForm = () => { const showAddForm = () => {
editingWebhook.value = undefined editingWebhook.value = undefined
@@ -82,33 +114,41 @@ const closeForm = () => {
editingWebhook.value = undefined editingWebhook.value = undefined
} }
const saveWebhook = (webhookData: Omit<Webhook, 'id' | 'createdAt' | 'updatedAt'>) => { const saveWebhook = async (webhookData: Omit<Webhook, 'id' | 'createdAt' | 'updatedAt'>) => {
if (editingWebhook.value) { try {
// Update existing webhook const serverId = route.params.id as string
const index = webhooks.value.findIndex(w => w.id === editingWebhook.value!.id) if (editingWebhook.value) {
if (index !== -1) { const response = await fetch(`/api/webhooks/${editingWebhook.value.id}`, {
webhooks.value[index] = { method: 'PUT',
...webhooks.value[index], headers: { 'Content-Type': 'application/json' },
...webhookData, body: JSON.stringify(webhookData)
updatedAt: new Date().toISOString() })
} if (!response.ok) throw new Error('Failed to update webhook')
} else {
const response = await fetch(`/api/servers/${serverId}/webhooks`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(webhookData)
})
if (!response.ok) throw new Error('Failed to create webhook')
} }
} else { closeForm()
// Add new webhook await fetchWebhooks()
const newWebhook: Webhook = { } catch (err) {
...webhookData, console.error('Save webhook error:', err)
id: Date.now().toString(), alert('Failed to save webhook. Check console for details.')
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
}
webhooks.value.push(newWebhook)
} }
closeForm()
} }
const deleteWebhook = (id: string) => { const deleteWebhook = async (id: string) => {
if (confirm('Are you sure you want to delete this webhook?')) { if (!confirm('Are you sure you want to delete this webhook?')) return
webhooks.value = webhooks.value.filter(w => w.id !== id) try {
const response = await fetch(`/api/webhooks/${id}`, { method: 'DELETE' })
if (!response.ok) throw new Error('Failed to delete webhook')
await fetchWebhooks()
} catch (err) {
console.error('Delete webhook error:', err)
alert('Failed to delete webhook. Check console for details.')
} }
} }
</script> </script>
View File