@donkeylabs/cli 0.1.1 → 0.4.1
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.
- package/package.json +2 -2
- package/src/client/base.ts +481 -0
- package/src/commands/generate.ts +242 -41
- package/templates/starter/src/index.ts +19 -30
- package/templates/starter/src/routes/health/handlers/ping.ts +22 -0
- package/templates/starter/src/routes/health/index.ts +16 -2
- package/templates/sveltekit-app/bun.lock +6 -6
- package/templates/sveltekit-app/donkeylabs.config.ts +1 -0
- package/templates/sveltekit-app/package.json +5 -5
- package/templates/sveltekit-app/src/lib/api.ts +229 -41
- package/templates/sveltekit-app/src/routes/+page.server.ts +2 -2
- package/templates/sveltekit-app/src/routes/+page.svelte +235 -96
- package/templates/sveltekit-app/src/server/index.ts +25 -117
- package/templates/sveltekit-app/src/server/routes/cache/handlers/delete.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/cache/handlers/get.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/cache/handlers/keys.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/cache/handlers/set.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/cache/index.ts +46 -0
- package/templates/sveltekit-app/src/server/routes/counter/handlers/decrement.ts +17 -0
- package/templates/sveltekit-app/src/server/routes/counter/handlers/get.ts +17 -0
- package/templates/sveltekit-app/src/server/routes/counter/handlers/increment.ts +17 -0
- package/templates/sveltekit-app/src/server/routes/counter/handlers/reset.ts +17 -0
- package/templates/sveltekit-app/src/server/routes/counter/index.ts +39 -0
- package/templates/sveltekit-app/src/server/routes/cron/handlers/list.ts +17 -0
- package/templates/sveltekit-app/src/server/routes/cron/index.ts +24 -0
- package/templates/sveltekit-app/src/server/routes/events/handlers/emit.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/events/index.ts +19 -0
- package/templates/sveltekit-app/src/server/routes/index.ts +8 -0
- package/templates/sveltekit-app/src/server/routes/jobs/handlers/enqueue.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/jobs/handlers/stats.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/jobs/index.ts +28 -0
- package/templates/sveltekit-app/src/server/routes/ratelimit/handlers/check.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/ratelimit/handlers/reset.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/ratelimit/index.ts +29 -0
- package/templates/sveltekit-app/src/server/routes/sse/handlers/broadcast.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/sse/handlers/clients.ts +15 -0
- package/templates/sveltekit-app/src/server/routes/sse/index.ts +28 -0
- package/templates/sveltekit-app/{svelte.config.js → svelte.config.ts} +3 -2
- package/templates/starter/CLAUDE.md +0 -144
- package/templates/starter/src/client.test.ts +0 -7
- package/templates/starter/src/db.ts +0 -9
- package/templates/starter/src/routes/health/ping/index.ts +0 -13
- package/templates/starter/src/routes/health/ping/models/model.ts +0 -23
- package/templates/starter/src/routes/health/ping/schema.ts +0 -14
- package/templates/starter/src/routes/health/ping/tests/integ.test.ts +0 -20
- package/templates/starter/src/routes/health/ping/tests/unit.test.ts +0 -21
- package/templates/starter/src/test-ctx.ts +0 -24
|
@@ -1,19 +1,38 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { browser } from
|
|
3
|
-
import { onMount } from
|
|
4
|
-
import { Button } from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
30
|
+
const client = createApi();
|
|
14
31
|
|
|
15
32
|
// SSE Events state
|
|
16
|
-
let events = $state<
|
|
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(
|
|
26
|
-
let cacheValue = $state(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
70
|
+
async function counterAction(
|
|
71
|
+
action: "get" | "increment" | "decrement" | "reset",
|
|
72
|
+
) {
|
|
52
73
|
counterLoading = true;
|
|
53
|
-
|
|
74
|
+
|
|
75
|
+
const result = await client.api.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:
|
|
82
|
+
await client.api.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.api.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:
|
|
93
|
+
await client.api.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.api.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:
|
|
105
|
+
const result = (await client.api.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.api.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.api.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:
|
|
132
|
+
await client.api.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.api.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.api.events.emit({ event: eventName, data: parsedData });
|
|
121
147
|
} catch (e) {
|
|
122
|
-
console.error(
|
|
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.
|
|
129
|
-
channel:
|
|
130
|
-
event:
|
|
154
|
+
await client.api.sse.broadcast({
|
|
155
|
+
channel: "events",
|
|
156
|
+
event: "manual",
|
|
131
157
|
data: {
|
|
132
158
|
id: Date.now(),
|
|
133
|
-
message:
|
|
159
|
+
message: "Manual broadcast!",
|
|
134
160
|
timestamp: new Date().toISOString(),
|
|
135
|
-
source:
|
|
136
|
-
}
|
|
161
|
+
source: "manual",
|
|
162
|
+
},
|
|
137
163
|
});
|
|
138
164
|
}
|
|
139
165
|
|
|
140
166
|
async function refreshSSEClients() {
|
|
141
|
-
sseClients = await api.
|
|
167
|
+
sseClients = (await client.api.sse.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 =
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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(
|
|
223
|
+
function getSourceColor(
|
|
224
|
+
source?: string,
|
|
225
|
+
): "default" | "secondary" | "destructive" | "outline" | "success" {
|
|
188
226
|
switch (source) {
|
|
189
|
-
case
|
|
190
|
-
|
|
191
|
-
case
|
|
192
|
-
|
|
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
|
|
199
|
-
|
|
200
|
-
case
|
|
201
|
-
|
|
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">
|
|
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 ?
|
|
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
|
|
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
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
272
|
-
|
|
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"
|
|
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">
|
|
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
|
|
299
|
-
|
|
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"
|
|
404
|
+
<Button onclick={resetRateLimit} size="sm" variant="outline"
|
|
405
|
+
>Reset</Button
|
|
406
|
+
>
|
|
304
407
|
</div>
|
|
305
408
|
{#if rateLimitResult}
|
|
306
|
-
<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
|
|
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"
|
|
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"
|
|
325
|
-
|
|
326
|
-
|
|
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">
|
|
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
|
|
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">
|
|
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
|
|
368
|
-
|
|
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
|
|
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 ?
|
|
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"
|
|
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">
|
|
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)}
|
|
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"
|
|
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>
|