mirror of
https://github.com/LukeHagar/relay.git
synced 2025-12-09 12:47:49 +00:00
Migrate to Svelte 5, SvelteKit 3, and Tailwind 4
Co-authored-by: lukeslakemail <lukeslakemail@gmail.com>
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "tailwindcss";
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
|
||||
143
sveltekit-app/src/lib/websocket-v5.ts
Normal file
143
sveltekit-app/src/lib/websocket-v5.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
export interface WebhookEvent {
|
||||
id: string;
|
||||
userId: string;
|
||||
method: string;
|
||||
path: string;
|
||||
query: string;
|
||||
body: string;
|
||||
headers: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface WebSocketState {
|
||||
connected: boolean;
|
||||
connecting: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
class WebSocketClient {
|
||||
private ws: WebSocket | null = null;
|
||||
private reconnectAttempts = 0;
|
||||
private maxReconnectAttempts = 5;
|
||||
private reconnectDelay = 1000;
|
||||
private url: string;
|
||||
|
||||
// Svelte 5 signals
|
||||
public events = $state<WebhookEvent[]>([]);
|
||||
public state = $state<WebSocketState>({
|
||||
connected: false,
|
||||
connecting: false,
|
||||
error: null
|
||||
});
|
||||
|
||||
constructor(url: string) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
connect() {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.state = { ...this.state, connecting: true, error: null };
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(this.url);
|
||||
|
||||
this.ws.onopen = () => {
|
||||
this.state = {
|
||||
connected: true,
|
||||
connecting: false,
|
||||
error: null
|
||||
};
|
||||
this.reconnectAttempts = 0;
|
||||
console.log('WebSocket connected');
|
||||
};
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.userId) {
|
||||
// This is a webhook event
|
||||
this.events = [data, ...this.events.slice(0, 99)]; // Keep last 100 events
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse WebSocket message:', error);
|
||||
}
|
||||
};
|
||||
|
||||
this.ws.onclose = () => {
|
||||
this.state = {
|
||||
connected: false,
|
||||
connecting: false
|
||||
};
|
||||
console.log('WebSocket disconnected');
|
||||
this.attemptReconnect();
|
||||
};
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
this.state = {
|
||||
connected: false,
|
||||
connecting: false,
|
||||
error: 'Connection failed'
|
||||
};
|
||||
console.error('WebSocket error:', error);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.state = {
|
||||
connected: false,
|
||||
connecting: false,
|
||||
error: 'Failed to create connection'
|
||||
};
|
||||
console.error('Failed to create WebSocket connection:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private attemptReconnect() {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
this.state = {
|
||||
...this.state,
|
||||
error: 'Max reconnection attempts reached'
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
this.reconnectAttempts++;
|
||||
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
||||
|
||||
setTimeout(() => {
|
||||
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
||||
this.connect();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
this.ws = null;
|
||||
}
|
||||
this.state = {
|
||||
connected: false,
|
||||
connecting: false,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
send(message: any) {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
|
||||
clearEvents() {
|
||||
this.events = [];
|
||||
}
|
||||
}
|
||||
|
||||
export function createWebSocketClient() {
|
||||
// Use the SvelteKit WebSocket endpoint
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const host = window.location.host;
|
||||
return new WebSocketClient(`${protocol}//${host}/api/ws`);
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
import { signIn, signOut } from '$lib/auth';
|
||||
import { Wifi, WifiOff, Settings, Zap, Users, Home } from 'lucide-svelte';
|
||||
|
||||
export let data;
|
||||
$: ({ session } = data);
|
||||
let { data } = $props();
|
||||
let { session } = data;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { createWebSocketClient, type WebhookEvent } from '$lib/websocket';
|
||||
import { createWebSocketClient, type WebhookEvent } from '$lib/websocket-v5';
|
||||
import { Activity, Zap, Users, Clock, TrendingUp, AlertCircle } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
let { data } = $props<{ data: PageData }>();
|
||||
|
||||
let wsClient: ReturnType<typeof createWebSocketClient>;
|
||||
let events: WebhookEvent[] = [];
|
||||
let stats = {
|
||||
let events = $state<WebhookEvent[]>([]);
|
||||
let stats = $state({
|
||||
totalEvents: 0,
|
||||
recentEvents: 0,
|
||||
activeConnections: 0,
|
||||
successRate: 0
|
||||
};
|
||||
});
|
||||
|
||||
let pathStats = {
|
||||
let pathStats = $state({
|
||||
topPaths: [],
|
||||
methodDistribution: [],
|
||||
recentPaths: []
|
||||
};
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (data.session?.user) {
|
||||
// Connect to SvelteKit WebSocket endpoint
|
||||
wsClient = createWebSocketClient();
|
||||
|
||||
wsClient.events.subscribe((newEvents) => {
|
||||
events = newEvents;
|
||||
updateStats();
|
||||
});
|
||||
// With Svelte 5 signals, we don't need to subscribe
|
||||
// The events signal will automatically update the UI
|
||||
|
||||
wsClient.connect();
|
||||
}
|
||||
@@ -37,6 +35,13 @@
|
||||
// Load initial data
|
||||
loadInitialData();
|
||||
|
||||
// Set up effect to update stats when events change
|
||||
$effect(() => {
|
||||
if (events.length > 0) {
|
||||
updateStats();
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (wsClient) {
|
||||
wsClient.disconnect();
|
||||
@@ -173,22 +178,22 @@
|
||||
<div class="card">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-lg font-medium text-gray-900">Recent Webhook Events</h2>
|
||||
{#if wsClient}
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="connection-status {$wsClient.state.connected ? 'connected' : $wsClient.state.connecting ? 'connecting' : 'disconnected'}">
|
||||
{#if $wsClient.state.connected}
|
||||
<Activity class="h-3 w-3 mr-1" />
|
||||
Connected
|
||||
{:else if $wsClient.state.connecting}
|
||||
<Clock class="h-3 w-3 mr-1" />
|
||||
Connecting...
|
||||
{:else}
|
||||
<AlertCircle class="h-3 w-3 mr-1" />
|
||||
Disconnected
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if wsClient}
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="connection-status {wsClient.state.connected ? 'connected' : wsClient.state.connecting ? 'connecting' : 'disconnected'}">
|
||||
{#if wsClient.state.connected}
|
||||
<Activity class="h-3 w-3 mr-1" />
|
||||
Connected
|
||||
{:else if wsClient.state.connecting}
|
||||
<Clock class="h-3 w-3 mr-1" />
|
||||
Connecting...
|
||||
{:else}
|
||||
<AlertCircle class="h-3 w-3 mr-1" />
|
||||
Disconnected
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if events.length === 0}
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
import { User, Key, Globe, Copy, Check, AlertCircle } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
let { data } = $props<{ data: PageData }>();
|
||||
|
||||
let user = data.user;
|
||||
let webhookUrl = '';
|
||||
let copied = false;
|
||||
let showSubdomainForm = false;
|
||||
let newSubdomain = '';
|
||||
let subdomainError = '';
|
||||
let user = $state(data.user);
|
||||
let webhookUrl = $state('');
|
||||
let copied = $state(false);
|
||||
let showSubdomainForm = $state(false);
|
||||
let newSubdomain = $state('');
|
||||
let subdomainError = $state('');
|
||||
|
||||
onMount(() => {
|
||||
if (user?.subdomain) {
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
import { Plus, Trash2, Edit, ExternalLink, ToggleLeft, ToggleRight } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
let { data } = $props<{ data: PageData }>();
|
||||
|
||||
let targets = data.targets;
|
||||
let showAddForm = false;
|
||||
let editingTarget: any = null;
|
||||
let formData = {
|
||||
let targets = $state(data.targets);
|
||||
let showAddForm = $state(false);
|
||||
let editingTarget = $state<any>(null);
|
||||
let formData = $state({
|
||||
target: '',
|
||||
nickname: ''
|
||||
};
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
// Initialize form
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { createWebSocketClient, type WebhookEvent } from '$lib/websocket';
|
||||
import { createWebSocketClient, type WebhookEvent } from '$lib/websocket-v5';
|
||||
import { Search, Filter, ChevronLeft, ChevronRight, Eye, Copy, Download } from 'lucide-svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
let { data } = $props<{ data: PageData }>();
|
||||
|
||||
let wsClient: ReturnType<typeof createWebSocketClient>;
|
||||
let events = data.events;
|
||||
let searchTerm = '';
|
||||
let selectedMethod = 'all';
|
||||
let selectedPath = 'all';
|
||||
let showFilters = false;
|
||||
let currentPage = data.page;
|
||||
let totalPages = data.totalPages;
|
||||
let events = $state(data.events);
|
||||
let searchTerm = $state('');
|
||||
let selectedMethod = $state('all');
|
||||
let selectedPath = $state('all');
|
||||
let showFilters = $state(false);
|
||||
let currentPage = $state(data.page);
|
||||
let totalPages = $state(data.totalPages);
|
||||
|
||||
const methods = ['all', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
||||
|
||||
@@ -24,10 +24,7 @@
|
||||
if (data.session?.user) {
|
||||
wsClient = createWebSocketClient();
|
||||
|
||||
wsClient.events.subscribe((newEvents) => {
|
||||
// Merge new events with existing ones
|
||||
events = [...newEvents, ...events].slice(0, 100);
|
||||
});
|
||||
// With Svelte 5 signals, events are automatically reactive
|
||||
|
||||
wsClient.connect();
|
||||
}
|
||||
@@ -105,17 +102,17 @@
|
||||
<Filter class="h-4 w-4 mr-2" />
|
||||
Filters
|
||||
</button>
|
||||
{#if wsClient}
|
||||
<div class="connection-status {$wsClient.state.connected ? 'connected' : $wsClient.state.connecting ? 'connecting' : 'disconnected'}">
|
||||
{#if $wsClient.state.connected}
|
||||
Live Updates
|
||||
{:else if $wsClient.state.connecting}
|
||||
Connecting...
|
||||
{:else}
|
||||
Disconnected
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if wsClient}
|
||||
<div class="connection-status {wsClient.state.connected ? 'connected' : wsClient.state.connecting ? 'connecting' : 'disconnected'}">
|
||||
{#if wsClient.state.connected}
|
||||
Live Updates
|
||||
{:else if wsClient.state.connecting}
|
||||
Connecting...
|
||||
{:else}
|
||||
Disconnected
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user