@donkeylabs/cli 0.1.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/package.json +2 -2
  2. package/src/client/base.ts +481 -0
  3. package/src/commands/generate.ts +242 -41
  4. package/templates/starter/src/index.ts +19 -30
  5. package/templates/starter/src/routes/health/handlers/ping.ts +22 -0
  6. package/templates/starter/src/routes/health/index.ts +16 -2
  7. package/templates/sveltekit-app/bun.lock +4 -4
  8. package/templates/sveltekit-app/donkeylabs.config.ts +1 -0
  9. package/templates/sveltekit-app/package.json +3 -3
  10. package/templates/sveltekit-app/src/lib/api.ts +230 -56
  11. package/templates/sveltekit-app/src/routes/+page.server.ts +2 -2
  12. package/templates/sveltekit-app/src/routes/+page.svelte +235 -96
  13. package/templates/sveltekit-app/src/server/index.ts +25 -117
  14. package/templates/sveltekit-app/src/server/routes/cache/handlers/delete.ts +15 -0
  15. package/templates/sveltekit-app/src/server/routes/cache/handlers/get.ts +15 -0
  16. package/templates/sveltekit-app/src/server/routes/cache/handlers/keys.ts +15 -0
  17. package/templates/sveltekit-app/src/server/routes/cache/handlers/set.ts +15 -0
  18. package/templates/sveltekit-app/src/server/routes/cache/index.ts +46 -0
  19. package/templates/sveltekit-app/src/server/routes/counter/handlers/decrement.ts +17 -0
  20. package/templates/sveltekit-app/src/server/routes/counter/handlers/get.ts +17 -0
  21. package/templates/sveltekit-app/src/server/routes/counter/handlers/increment.ts +17 -0
  22. package/templates/sveltekit-app/src/server/routes/counter/handlers/reset.ts +17 -0
  23. package/templates/sveltekit-app/src/server/routes/counter/index.ts +39 -0
  24. package/templates/sveltekit-app/src/server/routes/cron/handlers/list.ts +17 -0
  25. package/templates/sveltekit-app/src/server/routes/cron/index.ts +24 -0
  26. package/templates/sveltekit-app/src/server/routes/events/handlers/emit.ts +15 -0
  27. package/templates/sveltekit-app/src/server/routes/events/index.ts +19 -0
  28. package/templates/sveltekit-app/src/server/routes/index.ts +8 -0
  29. package/templates/sveltekit-app/src/server/routes/jobs/handlers/enqueue.ts +15 -0
  30. package/templates/sveltekit-app/src/server/routes/jobs/handlers/stats.ts +15 -0
  31. package/templates/sveltekit-app/src/server/routes/jobs/index.ts +28 -0
  32. package/templates/sveltekit-app/src/server/routes/ratelimit/handlers/check.ts +15 -0
  33. package/templates/sveltekit-app/src/server/routes/ratelimit/handlers/reset.ts +15 -0
  34. package/templates/sveltekit-app/src/server/routes/ratelimit/index.ts +29 -0
  35. package/templates/sveltekit-app/src/server/routes/sse/handlers/broadcast.ts +15 -0
  36. package/templates/sveltekit-app/src/server/routes/sse/handlers/clients.ts +15 -0
  37. package/templates/sveltekit-app/src/server/routes/sse/index.ts +28 -0
  38. package/templates/sveltekit-app/{svelte.config.js → svelte.config.ts} +3 -2
  39. package/templates/starter/CLAUDE.md +0 -144
  40. package/templates/starter/src/client.test.ts +0 -7
  41. package/templates/starter/src/db.ts +0 -9
  42. package/templates/starter/src/routes/health/ping/index.ts +0 -13
  43. package/templates/starter/src/routes/health/ping/models/model.ts +0 -23
  44. package/templates/starter/src/routes/health/ping/schema.ts +0 -14
  45. package/templates/starter/src/routes/health/ping/tests/integ.test.ts +0 -20
  46. package/templates/starter/src/routes/health/ping/tests/unit.test.ts +0 -21
  47. package/templates/starter/src/test-ctx.ts +0 -24
@@ -1,19 +1,38 @@
1
1
  <script lang="ts">
2
- import { browser } from '$app/environment';
3
- import { onMount } from 'svelte';
4
- import { Button } from '$lib/components/ui/button';
5
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '$lib/components/ui/card';
6
- import { Input } from '$lib/components/ui/input';
7
- import { Badge } from '$lib/components/ui/badge';
8
- import { createApi, type CronTask } from '$lib/api';
2
+ import { browser } from "$app/environment";
3
+ import { onMount } from "svelte";
4
+ import { Button } from "$lib/components/ui/button";
5
+ import type { Routes } from "$lib/api";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "$lib/components/ui/card";
13
+ import { Input } from "$lib/components/ui/input";
14
+ import { Badge } from "$lib/components/ui/badge";
15
+ import { createApi } from "$lib/api";
16
+
17
+ // Type for cron tasks returned by the API
18
+ interface CronTask {
19
+ id: string;
20
+ name: string;
21
+ expression: string;
22
+ enabled: boolean;
23
+ lastRun?: string;
24
+ nextRun?: string;
25
+ }
9
26
 
10
27
  let { data } = $props();
11
28
 
12
29
  // Create typed API client (browser mode - no locals)
13
- const api = createApi();
30
+ const client = createApi();
14
31
 
15
32
  // SSE Events state
16
- let events = $state<Array<{ id: number; message: string; timestamp: string; source?: string }>>([]);
33
+ let events = $state<
34
+ Array<{ id: number; message: string; timestamp: string; source?: string }>
35
+ >([]);
17
36
  let sseConnected = $state(false);
18
37
  let sseClients = $state({ total: 0, byChannel: 0 });
19
38
 
@@ -22,20 +41,20 @@
22
41
  let counterLoading = $state(false);
23
42
 
24
43
  // Cache state
25
- let cacheKey = $state('demo-key');
26
- let cacheValue = $state('Hello World');
44
+ let cacheKey = $state("demo-key");
45
+ let cacheValue = $state("Hello World");
27
46
  let cacheTTL = $state(30000);
28
47
  let cacheResult = $state<any>(null);
29
48
  let cacheKeys = $state<string[]>([]);
30
49
 
31
50
  // Jobs state
32
- let jobMessage = $state('Test job');
51
+ let jobMessage = $state("Test job");
33
52
  let jobDelay = $state(0);
34
53
  let jobStats = $state({ pending: 0, running: 0, completed: 0 });
35
54
  let lastJobId = $state<string | null>(null);
36
55
 
37
56
  // Rate Limiter state
38
- let rateLimitKey = $state('demo');
57
+ let rateLimitKey = $state("demo");
39
58
  let rateLimitMax = $state(5);
40
59
  let rateLimitWindow = $state(60000);
41
60
  let rateLimitResult = $state<any>(null);
@@ -44,72 +63,79 @@
44
63
  let cronTasks = $state<CronTask[]>([]);
45
64
 
46
65
  // Events (pub/sub) state
47
- let eventName = $state('demo.test');
66
+ let eventName = $state("demo.test");
48
67
  let eventData = $state('{"hello": "world"}');
49
68
 
50
69
  // Counter actions - using typed client
51
- async function counterAction(action: 'get' | 'increment' | 'decrement' | 'reset') {
70
+ async function counterAction(
71
+ action: "get" | "increment" | "decrement" | "reset",
72
+ ) {
52
73
  counterLoading = true;
53
- const result = await api.counter[action]();
74
+
75
+ const result = await client.counter[action]({});
54
76
  count = result.count;
55
77
  counterLoading = false;
56
78
  }
57
79
 
58
80
  // Cache actions - using typed client
59
81
  async function cacheSet() {
60
- await api.cache.set({ key: cacheKey, value: cacheValue, ttl: cacheTTL });
61
- cacheResult = { action: 'set', success: true };
82
+ await client.cache.set({ key: cacheKey, value: cacheValue, ttl: cacheTTL });
83
+ cacheResult = { action: "set", success: true };
62
84
  refreshCacheKeys();
63
85
  }
64
86
 
65
87
  async function cacheGet() {
66
- cacheResult = await api.cache.get({ key: cacheKey });
88
+ cacheResult = await client.cache.get({ key: cacheKey });
67
89
  refreshCacheKeys();
68
90
  }
69
91
 
70
92
  async function cacheDelete() {
71
- await api.cache.delete({ key: cacheKey });
72
- cacheResult = { action: 'deleted', success: true };
93
+ await client.cache.delete({ key: cacheKey });
94
+ cacheResult = { action: "deleted", success: true };
73
95
  refreshCacheKeys();
74
96
  }
75
97
 
76
98
  async function refreshCacheKeys() {
77
- const result = await api.cache.keys();
99
+ const result = await client.cache.keys({});
78
100
  cacheKeys = result.keys || [];
79
101
  }
80
102
 
81
103
  // Jobs actions - using typed client
82
104
  async function enqueueJob() {
83
- const result = await api.jobs.enqueue({
84
- name: 'demo-job',
105
+ const result = (await client.jobs.enqueue({
106
+ name: "demo-job",
85
107
  data: { message: jobMessage },
86
- delay: jobDelay > 0 ? jobDelay : undefined
87
- });
108
+ delay: jobDelay > 0 ? jobDelay : undefined,
109
+ })) as { jobId: string };
88
110
  lastJobId = result.jobId;
89
111
  refreshJobStats();
90
112
  }
91
113
 
92
114
  async function refreshJobStats() {
93
- jobStats = await api.jobs.stats();
115
+ jobStats = (await client.jobs.stats({})) as {
116
+ pending: number;
117
+ running: number;
118
+ completed: number;
119
+ };
94
120
  }
95
121
 
96
122
  // Rate limiter actions - using typed client
97
123
  async function checkRateLimit() {
98
- rateLimitResult = await api.ratelimit.check({
124
+ rateLimitResult = await client.ratelimit.check({
99
125
  key: rateLimitKey,
100
126
  limit: rateLimitMax,
101
- window: rateLimitWindow
127
+ window: rateLimitWindow,
102
128
  });
103
129
  }
104
130
 
105
131
  async function resetRateLimit() {
106
- await api.ratelimit.reset({ key: rateLimitKey });
107
- rateLimitResult = { reset: true, message: 'Rate limit reset' };
132
+ await client.ratelimit.reset({ key: rateLimitKey });
133
+ rateLimitResult = { reset: true, message: "Rate limit reset" };
108
134
  }
109
135
 
110
136
  // Cron actions - using typed client
111
137
  async function refreshCronTasks() {
112
- const result = await api.cron.list();
138
+ const result = (await client.cron.list({})) as { tasks: CronTask[] };
113
139
  cronTasks = result.tasks;
114
140
  }
115
141
 
@@ -117,28 +143,31 @@
117
143
  async function emitEvent() {
118
144
  try {
119
145
  const parsedData = JSON.parse(eventData);
120
- await api.events.emit({ event: eventName, data: parsedData });
146
+ await client.events.emit({ event: eventName, data: parsedData });
121
147
  } catch (e) {
122
- console.error('Invalid JSON:', e);
148
+ console.error("Invalid JSON:", e);
123
149
  }
124
150
  }
125
151
 
126
152
  // SSE actions - using typed client
127
153
  async function manualBroadcast() {
128
- await api.sseRoutes.broadcast({
129
- channel: 'events',
130
- event: 'manual',
154
+ await client.sseRoutes.broadcast({
155
+ channel: "events",
156
+ event: "manual",
131
157
  data: {
132
158
  id: Date.now(),
133
- message: 'Manual broadcast!',
159
+ message: "Manual broadcast!",
134
160
  timestamp: new Date().toISOString(),
135
- source: 'manual'
136
- }
161
+ source: "manual",
162
+ },
137
163
  });
138
164
  }
139
165
 
140
166
  async function refreshSSEClients() {
141
- sseClients = await api.sseRoutes.clients();
167
+ sseClients = (await client.sseRoutes.clients({})) as {
168
+ total: number;
169
+ byChannel: number;
170
+ };
142
171
  }
143
172
 
144
173
  onMount(() => {
@@ -151,17 +180,24 @@
151
180
  refreshSSEClients();
152
181
 
153
182
  // SSE subscription using the typed client
154
- const unsubscribe = api.sse.subscribe(['events'], (eventType, eventData) => {
155
- // Handle all event types
156
- if (['cron-event', 'job-completed', 'internal-event', 'manual'].includes(eventType)) {
157
- // Simple prepend - CSS handles animation via :first-child or key-based animation
158
- events = [{ ...eventData }, ...events].slice(0, 15);
159
-
160
- if (eventType === 'job-completed') {
161
- refreshJobStats();
183
+ const unsubscribe = client.sse.subscribe(
184
+ ["events"],
185
+ (eventType, eventData) => {
186
+ // Handle all event types
187
+ if (
188
+ ["cron-event", "job-completed", "internal-event", "manual"].includes(
189
+ eventType,
190
+ )
191
+ ) {
192
+ // Simple prepend - CSS handles animation via :first-child or key-based animation
193
+ events = [{ ...eventData }, ...events].slice(0, 15);
194
+
195
+ if (eventType === "job-completed") {
196
+ refreshJobStats();
197
+ }
162
198
  }
163
- }
164
- });
199
+ },
200
+ );
165
201
 
166
202
  // Track connection status
167
203
  const checkConnection = setInterval(() => {
@@ -184,21 +220,31 @@
184
220
  };
185
221
  });
186
222
 
187
- function getSourceColor(source?: string): "default" | "secondary" | "destructive" | "outline" | "success" {
223
+ function getSourceColor(
224
+ source?: string,
225
+ ): "default" | "secondary" | "destructive" | "outline" | "success" {
188
226
  switch (source) {
189
- case 'cron': return 'default';
190
- case 'manual': return 'secondary';
191
- case 'events': return 'outline';
192
- default: return 'success';
227
+ case "cron":
228
+ return "default";
229
+ case "manual":
230
+ return "secondary";
231
+ case "events":
232
+ return "outline";
233
+ default:
234
+ return "success";
193
235
  }
194
236
  }
195
237
 
196
238
  function getSourceLabel(source?: string) {
197
239
  switch (source) {
198
- case 'cron': return 'CRON';
199
- case 'manual': return 'MANUAL';
200
- case 'events': return 'PUB/SUB';
201
- default: return 'JOB';
240
+ case "cron":
241
+ return "CRON";
242
+ case "manual":
243
+ return "MANUAL";
244
+ case "events":
245
+ return "PUB/SUB";
246
+ default:
247
+ return "JOB";
202
248
  }
203
249
  }
204
250
  </script>
@@ -208,9 +254,11 @@
208
254
  <!-- Header -->
209
255
  <div class="text-center mb-8">
210
256
  <h1 class="text-3xl font-bold tracking-tight">@donkeylabs/server Demo</h1>
211
- <p class="text-muted-foreground mt-2">SvelteKit Adapter — All Core Services</p>
257
+ <p class="text-muted-foreground mt-2">
258
+ SvelteKit Adapter — All Core Services
259
+ </p>
212
260
  <Badge variant="outline" class="mt-3">
213
- SSR: {data.isSSR ? 'Yes' : 'No'} | Loaded: {data.loadedAt}
261
+ SSR: {data.isSSR ? "Yes" : "No"} | Loaded: {data.loadedAt}
214
262
  </Badge>
215
263
  </div>
216
264
 
@@ -220,17 +268,37 @@
220
268
  <Card>
221
269
  <CardHeader>
222
270
  <CardTitle>RPC Routes</CardTitle>
223
- <CardDescription>Type-safe API calls with Zod validation</CardDescription>
271
+ <CardDescription
272
+ >Type-safe API calls with Zod validation</CardDescription
273
+ >
224
274
  </CardHeader>
225
275
  <CardContent>
226
276
  <div class="text-center py-4">
227
277
  <span class="text-5xl font-bold text-primary">{count}</span>
228
278
  </div>
229
279
  <div class="flex gap-2 justify-center">
230
- <Button variant="outline" size="icon" onclick={() => counterAction('decrement')} disabled={counterLoading}>−</Button>
231
- <Button variant="secondary" onclick={() => counterAction('get')} disabled={counterLoading}>Refresh</Button>
232
- <Button variant="outline" size="icon" onclick={() => counterAction('increment')} disabled={counterLoading}>+</Button>
233
- <Button variant="ghost" onclick={() => counterAction('reset')} disabled={counterLoading}>Reset</Button>
280
+ <Button
281
+ variant="outline"
282
+ size="icon"
283
+ onclick={() => counterAction("decrement")}
284
+ disabled={counterLoading}>−</Button
285
+ >
286
+ <Button
287
+ variant="secondary"
288
+ onclick={() => counterAction("get")}
289
+ disabled={counterLoading}>Refresh</Button
290
+ >
291
+ <Button
292
+ variant="outline"
293
+ size="icon"
294
+ onclick={() => counterAction("increment")}
295
+ disabled={counterLoading}>+</Button
296
+ >
297
+ <Button
298
+ variant="ghost"
299
+ onclick={() => counterAction("reset")}
300
+ disabled={counterLoading}>Reset</Button
301
+ >
234
302
  </div>
235
303
  </CardContent>
236
304
  </Card>
@@ -248,14 +316,24 @@
248
316
  </div>
249
317
  <div class="flex gap-2">
250
318
  <Button onclick={cacheSet} size="sm">Set</Button>
251
- <Button onclick={cacheGet} size="sm" variant="secondary">Get</Button>
252
- <Button onclick={cacheDelete} size="sm" variant="outline">Delete</Button>
319
+ <Button onclick={cacheGet} size="sm" variant="secondary">Get</Button
320
+ >
321
+ <Button onclick={cacheDelete} size="sm" variant="outline"
322
+ >Delete</Button
323
+ >
253
324
  </div>
254
325
  {#if cacheResult}
255
- <pre class="text-xs bg-muted p-2 rounded-md overflow-auto">{JSON.stringify(cacheResult, null, 2)}</pre>
326
+ <pre
327
+ class="text-xs bg-muted p-2 rounded-md overflow-auto">{JSON.stringify(
328
+ cacheResult,
329
+ null,
330
+ 2,
331
+ )}</pre>
256
332
  {/if}
257
333
  <p class="text-xs text-muted-foreground">
258
- Keys ({cacheKeys.length}): {cacheKeys.length > 0 ? cacheKeys.join(', ') : 'none'}
334
+ Keys ({cacheKeys.length}): {cacheKeys.length > 0
335
+ ? cacheKeys.join(", ")
336
+ : "none"}
259
337
  </p>
260
338
  </CardContent>
261
339
  </Card>
@@ -268,15 +346,28 @@
268
346
  </CardHeader>
269
347
  <CardContent class="space-y-3">
270
348
  <div class="flex gap-2">
271
- <Input bind:value={jobMessage} placeholder="Job message" class="flex-1" />
272
- <Input bind:value={jobDelay} type="number" placeholder="Delay" class="w-20" />
349
+ <Input
350
+ bind:value={jobMessage}
351
+ placeholder="Job message"
352
+ class="flex-1"
353
+ />
354
+ <Input
355
+ bind:value={jobDelay}
356
+ type="number"
357
+ placeholder="Delay"
358
+ class="w-20"
359
+ />
273
360
  </div>
274
361
  <div class="flex gap-2">
275
362
  <Button onclick={enqueueJob} size="sm">Enqueue</Button>
276
- <Button onclick={refreshJobStats} size="sm" variant="outline">Refresh</Button>
363
+ <Button onclick={refreshJobStats} size="sm" variant="outline"
364
+ >Refresh</Button
365
+ >
277
366
  </div>
278
367
  {#if lastJobId}
279
- <p class="text-xs text-muted-foreground">Last Job: <code class="bg-muted px-1 rounded">{lastJobId}</code></p>
368
+ <p class="text-xs text-muted-foreground">
369
+ Last Job: <code class="bg-muted px-1 rounded">{lastJobId}</code>
370
+ </p>
280
371
  {/if}
281
372
  <div class="flex gap-3 text-xs text-muted-foreground">
282
373
  <span>Pending: {jobStats.pending}</span>
@@ -295,15 +386,31 @@
295
386
  <CardContent class="space-y-3">
296
387
  <div class="flex gap-2">
297
388
  <Input bind:value={rateLimitKey} placeholder="Key" class="flex-1" />
298
- <Input bind:value={rateLimitMax} type="number" placeholder="Limit" class="w-16" />
299
- <Input bind:value={rateLimitWindow} type="number" placeholder="Window" class="w-20" />
389
+ <Input
390
+ bind:value={rateLimitMax}
391
+ type="number"
392
+ placeholder="Limit"
393
+ class="w-16"
394
+ />
395
+ <Input
396
+ bind:value={rateLimitWindow}
397
+ type="number"
398
+ placeholder="Window"
399
+ class="w-20"
400
+ />
300
401
  </div>
301
402
  <div class="flex gap-2">
302
403
  <Button onclick={checkRateLimit} size="sm">Check</Button>
303
- <Button onclick={resetRateLimit} size="sm" variant="outline">Reset</Button>
404
+ <Button onclick={resetRateLimit} size="sm" variant="outline"
405
+ >Reset</Button
406
+ >
304
407
  </div>
305
408
  {#if rateLimitResult}
306
- <pre class="text-xs p-2 rounded-md overflow-auto {rateLimitResult.allowed === false ? 'bg-destructive/10 text-destructive' : 'bg-muted'}">{JSON.stringify(rateLimitResult, null, 2)}</pre>
409
+ <pre
410
+ class="text-xs p-2 rounded-md overflow-auto {rateLimitResult.allowed ===
411
+ false
412
+ ? 'bg-destructive/10 text-destructive'
413
+ : 'bg-muted'}">{JSON.stringify(rateLimitResult, null, 2)}</pre>
307
414
  {/if}
308
415
  </CardContent>
309
416
  </Card>
@@ -312,24 +419,35 @@
312
419
  <Card>
313
420
  <CardHeader>
314
421
  <CardTitle>Cron Jobs</CardTitle>
315
- <CardDescription>Scheduled tasks with cron expressions</CardDescription>
422
+ <CardDescription
423
+ >Scheduled tasks with cron expressions</CardDescription
424
+ >
316
425
  </CardHeader>
317
426
  <CardContent class="space-y-3">
318
- <Button onclick={refreshCronTasks} size="sm" variant="outline">Refresh Tasks</Button>
427
+ <Button onclick={refreshCronTasks} size="sm" variant="outline"
428
+ >Refresh Tasks</Button
429
+ >
319
430
  {#if cronTasks.length > 0}
320
431
  <ul class="space-y-2">
321
432
  {#each cronTasks as task}
322
433
  <li class="flex items-center gap-2 text-sm">
323
434
  <span class="font-medium">{task.name}</span>
324
- <code class="text-xs bg-muted px-1 rounded">{task.expression}</code>
325
- <Badge variant={task.enabled ? 'success' : 'secondary'} class="text-xs">
326
- {task.enabled ? 'Active' : 'Paused'}
435
+ <code class="text-xs bg-muted px-1 rounded"
436
+ >{task.expression}</code
437
+ >
438
+ <Badge
439
+ variant={task.enabled ? "success" : "secondary"}
440
+ class="text-xs"
441
+ >
442
+ {task.enabled ? "Active" : "Paused"}
327
443
  </Badge>
328
444
  </li>
329
445
  {/each}
330
446
  </ul>
331
447
  {:else}
332
- <p class="text-sm text-muted-foreground italic">No scheduled tasks</p>
448
+ <p class="text-sm text-muted-foreground italic">
449
+ No scheduled tasks
450
+ </p>
333
451
  {/if}
334
452
  </CardContent>
335
453
  </Card>
@@ -338,10 +456,14 @@
338
456
  <Card>
339
457
  <CardHeader>
340
458
  <CardTitle>Events (Pub/Sub)</CardTitle>
341
- <CardDescription>Internal event system with wildcards</CardDescription>
459
+ <CardDescription>Internal event system with wildcards</CardDescription
460
+ >
342
461
  </CardHeader>
343
462
  <CardContent class="space-y-3">
344
- <Input bind:value={eventName} placeholder="Event name (e.g., demo.test)" />
463
+ <Input
464
+ bind:value={eventName}
465
+ placeholder="Event name (e.g., demo.test)"
466
+ />
345
467
  <textarea
346
468
  bind:value={eventData}
347
469
  placeholder="JSON data"
@@ -349,7 +471,9 @@
349
471
  class="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
350
472
  ></textarea>
351
473
  <Button onclick={emitEvent} size="sm">Emit Event</Button>
352
- <p class="text-xs text-muted-foreground italic">Events matching "demo.*" broadcast to SSE</p>
474
+ <p class="text-xs text-muted-foreground italic">
475
+ Events matching "demo.*" broadcast to SSE
476
+ </p>
353
477
  </CardContent>
354
478
  </Card>
355
479
  </div>
@@ -364,33 +488,48 @@
364
488
  <div class="flex items-center gap-2">
365
489
  <span class="relative flex h-3 w-3">
366
490
  {#if sseConnected}
367
- <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
368
- <span class="relative inline-flex rounded-full h-3 w-3 bg-green-500"></span>
491
+ <span
492
+ class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
493
+ ></span>
494
+ <span
495
+ class="relative inline-flex rounded-full h-3 w-3 bg-green-500"
496
+ ></span>
369
497
  {:else}
370
- <span class="relative inline-flex rounded-full h-3 w-3 bg-gray-400"></span>
498
+ <span
499
+ class="relative inline-flex rounded-full h-3 w-3 bg-gray-400"
500
+ ></span>
371
501
  {/if}
372
502
  </span>
373
503
  <span class="text-sm text-muted-foreground">
374
- {sseConnected ? 'Connected' : 'Disconnected'} ({sseClients.byChannel} clients)
504
+ {sseConnected ? "Connected" : "Disconnected"} ({sseClients.byChannel}
505
+ clients)
375
506
  </span>
376
507
  </div>
377
508
  </CardHeader>
378
509
  <CardContent>
379
510
  <div class="flex gap-2 mb-4">
380
511
  <Button onclick={manualBroadcast} size="sm">Manual Broadcast</Button>
381
- <Button onclick={refreshSSEClients} size="sm" variant="outline">Refresh Clients</Button>
512
+ <Button onclick={refreshSSEClients} size="sm" variant="outline"
513
+ >Refresh Clients</Button
514
+ >
382
515
  </div>
383
516
  {#if events.length === 0}
384
- <p class="text-sm text-muted-foreground italic">Waiting for events... (cron broadcasts every 5s)</p>
517
+ <p class="text-sm text-muted-foreground italic">
518
+ Waiting for events... (cron broadcasts every 5s)
519
+ </p>
385
520
  {:else}
386
521
  <ul class="space-y-2 max-h-80 overflow-y-auto">
387
522
  {#each events as event}
388
523
  <li
389
524
  class="flex items-center gap-3 p-3 rounded-lg border bg-muted/50 animate-in slide-in-from-left-2 duration-300"
390
525
  >
391
- <Badge variant={getSourceColor(event.source)}>{getSourceLabel(event.source)}</Badge>
526
+ <Badge variant={getSourceColor(event.source)}
527
+ >{getSourceLabel(event.source)}</Badge
528
+ >
392
529
  <span class="flex-1 text-sm font-medium">{event.message}</span>
393
- <span class="text-xs text-muted-foreground">{new Date(event.timestamp).toLocaleTimeString()}</span>
530
+ <span class="text-xs text-muted-foreground"
531
+ >{new Date(event.timestamp).toLocaleTimeString()}</span
532
+ >
394
533
  </li>
395
534
  {/each}
396
535
  </ul>