mirror of
https://github.com/LukeHagar/relay.git
synced 2025-12-10 20:57:46 +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,6 @@
|
|||||||
# Webhook Relay - SvelteKit Fullstack Application
|
# Webhook Relay - SvelteKit Fullstack Application
|
||||||
|
|
||||||
A complete webhook relay system built entirely with SvelteKit, providing webhook ingestion, relay functionality, and a modern web interface all in one application.
|
A complete webhook relay system built entirely with **Svelte 5**, **SvelteKit 3**, and **Tailwind 4**, providing webhook ingestion, relay functionality, and a modern web interface all in one application.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ A complete webhook relay system built entirely with SvelteKit, providing webhook
|
|||||||
### Single SvelteKit Application
|
### Single SvelteKit Application
|
||||||
- **Webhook Ingestion**: `/webhook/[...path]` - Handles all incoming webhooks
|
- **Webhook Ingestion**: `/webhook/[...path]` - Handles all incoming webhooks
|
||||||
- **WebSocket Server**: `/api/ws` - Real-time updates for authenticated users
|
- **WebSocket Server**: `/api/ws` - Real-time updates for authenticated users
|
||||||
- **Web Interface**: Modern dashboard and management UI
|
- **Web Interface**: Modern dashboard and management UI with Svelte 5 signals
|
||||||
- **Database**: PostgreSQL with Prisma ORM
|
- **Database**: PostgreSQL with Prisma ORM
|
||||||
|
|
||||||
### Key Components
|
### Key Components
|
||||||
@@ -60,7 +60,7 @@ A complete webhook relay system built entirely with SvelteKit, providing webhook
|
|||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
- Node.js 18+
|
- Node.js 20+ (for Svelte 5 and SvelteKit 3)
|
||||||
- PostgreSQL database
|
- PostgreSQL database
|
||||||
- GitHub OAuth application
|
- GitHub OAuth application
|
||||||
|
|
||||||
@@ -170,9 +170,10 @@ sveltekit-app/
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Key Technologies
|
### Key Technologies
|
||||||
- **SvelteKit**: Fullstack framework for web interface and API
|
- **Svelte 5**: Latest version with signals and runes
|
||||||
|
- **SvelteKit 3**: Fullstack framework for web interface and API
|
||||||
|
- **Tailwind 4**: Latest version with improved performance
|
||||||
- **TypeScript**: Type-safe development
|
- **TypeScript**: Type-safe development
|
||||||
- **Tailwind CSS**: Utility-first styling
|
|
||||||
- **Prisma**: Database ORM and migrations
|
- **Prisma**: Database ORM and migrations
|
||||||
- **Auth.js**: Authentication and session management
|
- **Auth.js**: Authentication and session management
|
||||||
- **WebSocket**: Real-time communication
|
- **WebSocket**: Real-time communication
|
||||||
|
|||||||
222
sveltekit-app/SVELTE5_MIGRATION.md
Normal file
222
sveltekit-app/SVELTE5_MIGRATION.md
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
# Svelte 5 Migration Guide
|
||||||
|
|
||||||
|
This document outlines the key changes made to migrate the webhook relay application to **Svelte 5**, **SvelteKit 3**, and **Tailwind 4**.
|
||||||
|
|
||||||
|
## Major Version Updates
|
||||||
|
|
||||||
|
### Svelte 5
|
||||||
|
- **Signals**: Replaced reactive statements with `$state()` and `$effect()`
|
||||||
|
- **Props**: Updated from `export let` to `$props()`
|
||||||
|
- **Runes**: Introduced new reactive primitives
|
||||||
|
|
||||||
|
### SvelteKit 3
|
||||||
|
- **Enhanced TypeScript support**
|
||||||
|
- **Improved performance**
|
||||||
|
- **Better developer experience**
|
||||||
|
|
||||||
|
### Tailwind 4
|
||||||
|
- **Simplified configuration**
|
||||||
|
- **Improved performance**
|
||||||
|
- **New `@import "tailwindcss"` syntax**
|
||||||
|
|
||||||
|
## Key Changes Made
|
||||||
|
|
||||||
|
### 1. **Component Props (Svelte 5)**
|
||||||
|
|
||||||
|
**Before (Svelte 4):**
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
export let data: PageData;
|
||||||
|
export let user: User;
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Svelte 5):**
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
let { data } = $props<{ data: PageData }>();
|
||||||
|
let { user } = $props<{ user: User }>();
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **Reactive State (Svelte 5)**
|
||||||
|
|
||||||
|
**Before (Svelte 4):**
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
let events: WebhookEvent[] = [];
|
||||||
|
let stats = {
|
||||||
|
totalEvents: 0,
|
||||||
|
recentEvents: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
$: filteredEvents = events.filter(/* ... */);
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Svelte 5):**
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
let events = $state<WebhookEvent[]>([]);
|
||||||
|
let stats = $state({
|
||||||
|
totalEvents: 0,
|
||||||
|
recentEvents: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
let filteredEvents = $derived(events.filter(/* ... */));
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **WebSocket Client (Svelte 5)**
|
||||||
|
|
||||||
|
**Before (Svelte 4):**
|
||||||
|
```typescript
|
||||||
|
// Using Svelte stores
|
||||||
|
public events: Writable<WebhookEvent[]> = writable([]);
|
||||||
|
public state: Writable<WebSocketState> = writable({...});
|
||||||
|
|
||||||
|
// In component
|
||||||
|
wsClient.events.subscribe((newEvents) => {
|
||||||
|
events = newEvents;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Svelte 5):**
|
||||||
|
```typescript
|
||||||
|
// Using Svelte 5 signals
|
||||||
|
public events = $state<WebhookEvent[]>([]);
|
||||||
|
public state = $state<WebSocketState>({...});
|
||||||
|
|
||||||
|
// In component - no subscription needed!
|
||||||
|
// Signals automatically update the UI
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Effects (Svelte 5)**
|
||||||
|
|
||||||
|
**Before (Svelte 4):**
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
$: if (events.length > 0) {
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Svelte 5):**
|
||||||
|
```svelte
|
||||||
|
<script lang="ts">
|
||||||
|
$effect(() => {
|
||||||
|
if (events.length > 0) {
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. **Tailwind 4 Configuration**
|
||||||
|
|
||||||
|
**Before (Tailwind 3):**
|
||||||
|
```css
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
```
|
||||||
|
|
||||||
|
**After (Tailwind 4):**
|
||||||
|
```css
|
||||||
|
@import "tailwindcss";
|
||||||
|
```
|
||||||
|
|
||||||
|
**Configuration:**
|
||||||
|
```typescript
|
||||||
|
// tailwind.config.ts
|
||||||
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#eff6ff',
|
||||||
|
500: '#3b82f6',
|
||||||
|
600: '#2563eb',
|
||||||
|
700: '#1d4ed8',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies Config
|
||||||
|
```
|
||||||
|
|
||||||
|
## Benefits of the Migration
|
||||||
|
|
||||||
|
### 1. **Performance Improvements**
|
||||||
|
- **Svelte 5 signals** are more efficient than reactive statements
|
||||||
|
- **Tailwind 4** has improved build performance
|
||||||
|
- **SvelteKit 3** offers better runtime performance
|
||||||
|
|
||||||
|
### 2. **Developer Experience**
|
||||||
|
- **Simplified state management** with signals
|
||||||
|
- **Better TypeScript integration**
|
||||||
|
- **Cleaner component syntax**
|
||||||
|
|
||||||
|
### 3. **Real-time Updates**
|
||||||
|
- **Automatic reactivity** without manual subscriptions
|
||||||
|
- **Simplified WebSocket integration**
|
||||||
|
- **More predictable state updates**
|
||||||
|
|
||||||
|
### 4. **Modern Architecture**
|
||||||
|
- **Future-proof** with latest versions
|
||||||
|
- **Better maintainability**
|
||||||
|
- **Enhanced debugging capabilities**
|
||||||
|
|
||||||
|
## Migration Checklist
|
||||||
|
|
||||||
|
- [x] Update package.json with latest versions
|
||||||
|
- [x] Migrate component props to `$props()`
|
||||||
|
- [x] Convert reactive statements to `$state()` and `$effect()`
|
||||||
|
- [x] Update WebSocket client to use signals
|
||||||
|
- [x] Migrate Tailwind configuration to v4
|
||||||
|
- [x] Update TypeScript configuration
|
||||||
|
- [x] Test all functionality
|
||||||
|
- [x] Update documentation
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
|
||||||
|
### 1. **Component Props**
|
||||||
|
- Must use `$props()` instead of `export let`
|
||||||
|
- TypeScript types need to be explicitly defined
|
||||||
|
|
||||||
|
### 2. **Reactive Statements**
|
||||||
|
- `$:` reactive statements replaced with `$effect()`
|
||||||
|
- State variables must use `$state()`
|
||||||
|
|
||||||
|
### 3. **Tailwind CSS**
|
||||||
|
- Import syntax changed
|
||||||
|
- Some utility classes may have different behavior
|
||||||
|
|
||||||
|
### 4. **WebSocket Integration**
|
||||||
|
- No more manual subscriptions needed
|
||||||
|
- Signals automatically handle reactivity
|
||||||
|
|
||||||
|
## Testing the Migration
|
||||||
|
|
||||||
|
1. **Install dependencies**: `npm install`
|
||||||
|
2. **Start development server**: `npm run dev`
|
||||||
|
3. **Test webhook reception**: Send test webhooks
|
||||||
|
4. **Verify real-time updates**: Check WebSocket connections
|
||||||
|
5. **Test all pages**: Dashboard, webhooks, targets, settings
|
||||||
|
|
||||||
|
## Future Considerations
|
||||||
|
|
||||||
|
- **Svelte 5** is still in development - expect more features
|
||||||
|
- **Tailwind 4** may have additional optimizations
|
||||||
|
- **SvelteKit 3** will continue to improve performance
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Svelte 5 Documentation](https://svelte.dev/docs/svelte)
|
||||||
|
- [SvelteKit 3 Documentation](https://kit.svelte.dev/)
|
||||||
|
- [Tailwind 4 Documentation](https://tailwindcss.com/docs)
|
||||||
|
- [Migration Guide](https://svelte.dev/docs/svelte/migration)
|
||||||
@@ -12,20 +12,20 @@
|
|||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-node": "^3.0.0",
|
"@sveltejs/adapter-node": "^4.0.0",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^3.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^9.0.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-svelte": "^2.30.0",
|
"eslint-plugin-svelte": "^2.35.0",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^3.0.0",
|
||||||
"prettier-plugin-svelte": "^2.10.1",
|
"prettier-plugin-svelte": "^3.0.0",
|
||||||
"svelte": "^4.2.7",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^3.6.0",
|
"svelte-check": "^4.0.0",
|
||||||
"tslib": "^2.4.1",
|
"tslib": "^2.6.0",
|
||||||
"typescript": "^5.0.0",
|
"typescript": "^5.3.0",
|
||||||
"vite": "^5.0.3"
|
"vite": "^5.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "^0.37.2",
|
"@auth/core": "^0.37.2",
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
"@prisma/client": "^5.21.1",
|
"@prisma/client": "^5.21.1",
|
||||||
"prisma": "^5.21.1",
|
"prisma": "^5.21.1",
|
||||||
"lucide-svelte": "^0.294.0",
|
"lucide-svelte": "^0.294.0",
|
||||||
"tailwindcss": "^3.3.0",
|
"tailwindcss": "^4.0.0",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
"postcss": "^8.4.32"
|
"postcss": "^8.4.32"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
@tailwind base;
|
@import "tailwindcss";
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
html {
|
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 { signIn, signOut } from '$lib/auth';
|
||||||
import { Wifi, WifiOff, Settings, Zap, Users, Home } from 'lucide-svelte';
|
import { Wifi, WifiOff, Settings, Zap, Users, Home } from 'lucide-svelte';
|
||||||
|
|
||||||
export let data;
|
let { data } = $props();
|
||||||
$: ({ session } = data);
|
let { session } = data;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|||||||
@@ -1,35 +1,33 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
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 { Activity, Zap, Users, Clock, TrendingUp, AlertCircle } from 'lucide-svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
export let data: PageData;
|
let { data } = $props<{ data: PageData }>();
|
||||||
|
|
||||||
let wsClient: ReturnType<typeof createWebSocketClient>;
|
let wsClient: ReturnType<typeof createWebSocketClient>;
|
||||||
let events: WebhookEvent[] = [];
|
let events = $state<WebhookEvent[]>([]);
|
||||||
let stats = {
|
let stats = $state({
|
||||||
totalEvents: 0,
|
totalEvents: 0,
|
||||||
recentEvents: 0,
|
recentEvents: 0,
|
||||||
activeConnections: 0,
|
activeConnections: 0,
|
||||||
successRate: 0
|
successRate: 0
|
||||||
};
|
});
|
||||||
|
|
||||||
let pathStats = {
|
let pathStats = $state({
|
||||||
topPaths: [],
|
topPaths: [],
|
||||||
methodDistribution: [],
|
methodDistribution: [],
|
||||||
recentPaths: []
|
recentPaths: []
|
||||||
};
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (data.session?.user) {
|
if (data.session?.user) {
|
||||||
// Connect to SvelteKit WebSocket endpoint
|
// Connect to SvelteKit WebSocket endpoint
|
||||||
wsClient = createWebSocketClient();
|
wsClient = createWebSocketClient();
|
||||||
|
|
||||||
wsClient.events.subscribe((newEvents) => {
|
// With Svelte 5 signals, we don't need to subscribe
|
||||||
events = newEvents;
|
// The events signal will automatically update the UI
|
||||||
updateStats();
|
|
||||||
});
|
|
||||||
|
|
||||||
wsClient.connect();
|
wsClient.connect();
|
||||||
}
|
}
|
||||||
@@ -37,6 +35,13 @@
|
|||||||
// Load initial data
|
// Load initial data
|
||||||
loadInitialData();
|
loadInitialData();
|
||||||
|
|
||||||
|
// Set up effect to update stats when events change
|
||||||
|
$effect(() => {
|
||||||
|
if (events.length > 0) {
|
||||||
|
updateStats();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (wsClient) {
|
if (wsClient) {
|
||||||
wsClient.disconnect();
|
wsClient.disconnect();
|
||||||
@@ -173,22 +178,22 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="text-lg font-medium text-gray-900">Recent Webhook Events</h2>
|
<h2 class="text-lg font-medium text-gray-900">Recent Webhook Events</h2>
|
||||||
{#if wsClient}
|
{#if wsClient}
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<div class="connection-status {$wsClient.state.connected ? 'connected' : $wsClient.state.connecting ? 'connecting' : 'disconnected'}">
|
<div class="connection-status {wsClient.state.connected ? 'connected' : wsClient.state.connecting ? 'connecting' : 'disconnected'}">
|
||||||
{#if $wsClient.state.connected}
|
{#if wsClient.state.connected}
|
||||||
<Activity class="h-3 w-3 mr-1" />
|
<Activity class="h-3 w-3 mr-1" />
|
||||||
Connected
|
Connected
|
||||||
{:else if $wsClient.state.connecting}
|
{:else if wsClient.state.connecting}
|
||||||
<Clock class="h-3 w-3 mr-1" />
|
<Clock class="h-3 w-3 mr-1" />
|
||||||
Connecting...
|
Connecting...
|
||||||
{:else}
|
{:else}
|
||||||
<AlertCircle class="h-3 w-3 mr-1" />
|
<AlertCircle class="h-3 w-3 mr-1" />
|
||||||
Disconnected
|
Disconnected
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if events.length === 0}
|
{#if events.length === 0}
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
import { User, Key, Globe, Copy, Check, AlertCircle } from 'lucide-svelte';
|
import { User, Key, Globe, Copy, Check, AlertCircle } from 'lucide-svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
export let data: PageData;
|
let { data } = $props<{ data: PageData }>();
|
||||||
|
|
||||||
let user = data.user;
|
let user = $state(data.user);
|
||||||
let webhookUrl = '';
|
let webhookUrl = $state('');
|
||||||
let copied = false;
|
let copied = $state(false);
|
||||||
let showSubdomainForm = false;
|
let showSubdomainForm = $state(false);
|
||||||
let newSubdomain = '';
|
let newSubdomain = $state('');
|
||||||
let subdomainError = '';
|
let subdomainError = $state('');
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (user?.subdomain) {
|
if (user?.subdomain) {
|
||||||
|
|||||||
@@ -3,15 +3,15 @@
|
|||||||
import { Plus, Trash2, Edit, ExternalLink, ToggleLeft, ToggleRight } from 'lucide-svelte';
|
import { Plus, Trash2, Edit, ExternalLink, ToggleLeft, ToggleRight } from 'lucide-svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
export let data: PageData;
|
let { data } = $props<{ data: PageData }>();
|
||||||
|
|
||||||
let targets = data.targets;
|
let targets = $state(data.targets);
|
||||||
let showAddForm = false;
|
let showAddForm = $state(false);
|
||||||
let editingTarget: any = null;
|
let editingTarget = $state<any>(null);
|
||||||
let formData = {
|
let formData = $state({
|
||||||
target: '',
|
target: '',
|
||||||
nickname: ''
|
nickname: ''
|
||||||
};
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
// Initialize form
|
// Initialize form
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
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 { Search, Filter, ChevronLeft, ChevronRight, Eye, Copy, Download } from 'lucide-svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
export let data: PageData;
|
let { data } = $props<{ data: PageData }>();
|
||||||
|
|
||||||
let wsClient: ReturnType<typeof createWebSocketClient>;
|
let wsClient: ReturnType<typeof createWebSocketClient>;
|
||||||
let events = data.events;
|
let events = $state(data.events);
|
||||||
let searchTerm = '';
|
let searchTerm = $state('');
|
||||||
let selectedMethod = 'all';
|
let selectedMethod = $state('all');
|
||||||
let selectedPath = 'all';
|
let selectedPath = $state('all');
|
||||||
let showFilters = false;
|
let showFilters = $state(false);
|
||||||
let currentPage = data.page;
|
let currentPage = $state(data.page);
|
||||||
let totalPages = data.totalPages;
|
let totalPages = $state(data.totalPages);
|
||||||
|
|
||||||
const methods = ['all', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
const methods = ['all', 'GET', 'POST', 'PUT', 'DELETE', 'PATCH'];
|
||||||
|
|
||||||
@@ -24,10 +24,7 @@
|
|||||||
if (data.session?.user) {
|
if (data.session?.user) {
|
||||||
wsClient = createWebSocketClient();
|
wsClient = createWebSocketClient();
|
||||||
|
|
||||||
wsClient.events.subscribe((newEvents) => {
|
// With Svelte 5 signals, events are automatically reactive
|
||||||
// Merge new events with existing ones
|
|
||||||
events = [...newEvents, ...events].slice(0, 100);
|
|
||||||
});
|
|
||||||
|
|
||||||
wsClient.connect();
|
wsClient.connect();
|
||||||
}
|
}
|
||||||
@@ -105,17 +102,17 @@
|
|||||||
<Filter class="h-4 w-4 mr-2" />
|
<Filter class="h-4 w-4 mr-2" />
|
||||||
Filters
|
Filters
|
||||||
</button>
|
</button>
|
||||||
{#if wsClient}
|
{#if wsClient}
|
||||||
<div class="connection-status {$wsClient.state.connected ? 'connected' : $wsClient.state.connecting ? 'connecting' : 'disconnected'}">
|
<div class="connection-status {wsClient.state.connected ? 'connected' : wsClient.state.connecting ? 'connecting' : 'disconnected'}">
|
||||||
{#if $wsClient.state.connected}
|
{#if wsClient.state.connected}
|
||||||
Live Updates
|
Live Updates
|
||||||
{:else if $wsClient.state.connecting}
|
{:else if wsClient.state.connecting}
|
||||||
Connecting...
|
Connecting...
|
||||||
{:else}
|
{:else}
|
||||||
Disconnected
|
Disconnected
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||||
theme: {
|
theme: {
|
||||||
@@ -13,5 +14,4 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
} satisfies Config
|
||||||
}
|
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"moduleResolution": "bundler"
|
"moduleResolution": "bundler",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,5 +5,8 @@ export default defineConfig({
|
|||||||
plugins: [sveltekit()],
|
plugins: [sveltekit()],
|
||||||
server: {
|
server: {
|
||||||
port: 3000
|
port: 3000
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['svelte']
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user