@omnixal/openclaw-nats-plugin 0.2.1 → 0.2.3
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/cli/docker-setup.ts +38 -2
- package/cli/paths.ts +1 -0
- package/cli/setup.ts +45 -2
- package/dashboard/bun.lock +253 -0
- package/dashboard/components.json +16 -0
- package/dashboard/index.html +22 -0
- package/dashboard/package.json +24 -0
- package/dashboard/src/App.svelte +107 -0
- package/dashboard/src/app.css +232 -0
- package/dashboard/src/lib/ConfigPanel.svelte +35 -0
- package/dashboard/src/lib/CronPanel.svelte +255 -0
- package/dashboard/src/lib/HealthCards.svelte +68 -0
- package/dashboard/src/lib/MetricsPanel.svelte +60 -0
- package/dashboard/src/lib/PendingTable.svelte +73 -0
- package/dashboard/src/lib/RoutesPanel.svelte +178 -0
- package/dashboard/src/lib/ThemeToggle.svelte +54 -0
- package/dashboard/src/lib/api.ts +141 -0
- package/dashboard/src/lib/components/ui/badge/badge.svelte +50 -0
- package/dashboard/src/lib/components/ui/badge/index.ts +2 -0
- package/dashboard/src/lib/components/ui/button/button.svelte +82 -0
- package/dashboard/src/lib/components/ui/button/index.ts +17 -0
- package/dashboard/src/lib/components/ui/card/card-action.svelte +20 -0
- package/dashboard/src/lib/components/ui/card/card-content.svelte +15 -0
- package/dashboard/src/lib/components/ui/card/card-description.svelte +20 -0
- package/dashboard/src/lib/components/ui/card/card-footer.svelte +20 -0
- package/dashboard/src/lib/components/ui/card/card-header.svelte +23 -0
- package/dashboard/src/lib/components/ui/card/card-title.svelte +20 -0
- package/dashboard/src/lib/components/ui/card/card.svelte +23 -0
- package/dashboard/src/lib/components/ui/card/index.ts +25 -0
- package/dashboard/src/lib/components/ui/table/index.ts +28 -0
- package/dashboard/src/lib/components/ui/table/table-body.svelte +20 -0
- package/dashboard/src/lib/components/ui/table/table-caption.svelte +20 -0
- package/dashboard/src/lib/components/ui/table/table-cell.svelte +23 -0
- package/dashboard/src/lib/components/ui/table/table-footer.svelte +20 -0
- package/dashboard/src/lib/components/ui/table/table-head.svelte +23 -0
- package/dashboard/src/lib/components/ui/table/table-header.svelte +20 -0
- package/dashboard/src/lib/components/ui/table/table-row.svelte +23 -0
- package/dashboard/src/lib/components/ui/table/table.svelte +22 -0
- package/dashboard/src/lib/utils.ts +29 -0
- package/dashboard/src/main.ts +7 -0
- package/dashboard/tsconfig.json +19 -0
- package/dashboard/vite.config.ts +30 -0
- package/package.json +5 -4
- package/plugins/nats-context-engine/http-handler.ts +8 -2
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Table from '$lib/components/ui/table';
|
|
3
|
+
import { Badge } from '$lib/components/ui/badge';
|
|
4
|
+
import { Button } from '$lib/components/ui/button';
|
|
5
|
+
import { type PendingEvent, markDelivered } from '$lib/api';
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
events: PendingEvent[];
|
|
9
|
+
onRefresh: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { events, onRefresh }: Props = $props();
|
|
13
|
+
|
|
14
|
+
function relativeAge(ts: number): string {
|
|
15
|
+
const seconds = Math.floor((Date.now() - ts) / 1000);
|
|
16
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
17
|
+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
|
|
18
|
+
return `${Math.floor(seconds / 3600)}h ago`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function priorityVariant(p: number): 'default' | 'secondary' | 'destructive' {
|
|
22
|
+
if (p >= 8) return 'destructive';
|
|
23
|
+
if (p >= 5) return 'default';
|
|
24
|
+
return 'secondary';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let ackError: string | null = $state(null);
|
|
28
|
+
|
|
29
|
+
async function ack(id: string) {
|
|
30
|
+
try {
|
|
31
|
+
ackError = null;
|
|
32
|
+
await markDelivered([id]);
|
|
33
|
+
onRefresh();
|
|
34
|
+
} catch (e: any) {
|
|
35
|
+
ackError = e.message;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
{#if ackError}
|
|
41
|
+
<div class="rounded-md bg-destructive/10 p-2 text-xs text-destructive mb-2">{ackError}</div>
|
|
42
|
+
{/if}
|
|
43
|
+
|
|
44
|
+
{#if events.length === 0}
|
|
45
|
+
<p class="text-sm text-muted-foreground py-4">No pending events</p>
|
|
46
|
+
{:else}
|
|
47
|
+
<Table.Root>
|
|
48
|
+
<Table.Header>
|
|
49
|
+
<Table.Row>
|
|
50
|
+
<Table.Head>Subject</Table.Head>
|
|
51
|
+
<Table.Head>Priority</Table.Head>
|
|
52
|
+
<Table.Head>Age</Table.Head>
|
|
53
|
+
<Table.Head class="w-16"></Table.Head>
|
|
54
|
+
</Table.Row>
|
|
55
|
+
</Table.Header>
|
|
56
|
+
<Table.Body>
|
|
57
|
+
{#each events as event}
|
|
58
|
+
<Table.Row>
|
|
59
|
+
<Table.Cell class="font-mono text-xs">{event.subject}</Table.Cell>
|
|
60
|
+
<Table.Cell>
|
|
61
|
+
<Badge variant={priorityVariant(event.priority)}>{event.priority}</Badge>
|
|
62
|
+
</Table.Cell>
|
|
63
|
+
<Table.Cell class="text-xs text-muted-foreground">
|
|
64
|
+
{relativeAge(event.createdAt)}
|
|
65
|
+
</Table.Cell>
|
|
66
|
+
<Table.Cell>
|
|
67
|
+
<Button variant="ghost" size="sm" onclick={() => ack(event.id)}>Ack</Button>
|
|
68
|
+
</Table.Cell>
|
|
69
|
+
</Table.Row>
|
|
70
|
+
{/each}
|
|
71
|
+
</Table.Body>
|
|
72
|
+
</Table.Root>
|
|
73
|
+
{/if}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import * as Card from '$lib/components/ui/card';
|
|
3
|
+
import * as Table from '$lib/components/ui/table';
|
|
4
|
+
import { Badge } from '$lib/components/ui/badge';
|
|
5
|
+
import { Button } from '$lib/components/ui/button';
|
|
6
|
+
import { type EventRoute, createRoute, deleteRoute } from '$lib/api';
|
|
7
|
+
import { relativeAge, formatDuration } from '$lib/utils';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
routes: EventRoute[];
|
|
11
|
+
onRefresh: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { routes, onRefresh }: Props = $props();
|
|
15
|
+
|
|
16
|
+
let showForm: boolean = $state(false);
|
|
17
|
+
let formPattern: string = $state('agent.events.');
|
|
18
|
+
let formTarget: string = $state('main');
|
|
19
|
+
let formPriority: number = $state(5);
|
|
20
|
+
let formError: string | null = $state(null);
|
|
21
|
+
let actionError: string | null = $state(null);
|
|
22
|
+
let loading: boolean = $state(false);
|
|
23
|
+
|
|
24
|
+
function priorityVariant(p: number): 'default' | 'secondary' | 'destructive' {
|
|
25
|
+
if (p >= 8) return 'destructive';
|
|
26
|
+
if (p >= 5) return 'default';
|
|
27
|
+
return 'secondary';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resetForm() {
|
|
31
|
+
formPattern = 'agent.events.';
|
|
32
|
+
formTarget = 'main';
|
|
33
|
+
formPriority = 5;
|
|
34
|
+
formError = null;
|
|
35
|
+
showForm = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function handleCreate() {
|
|
39
|
+
formError = null;
|
|
40
|
+
if (!formPattern.startsWith('agent.events.')) {
|
|
41
|
+
formError = 'Pattern must start with "agent.events."';
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
loading = true;
|
|
46
|
+
await createRoute({
|
|
47
|
+
pattern: formPattern,
|
|
48
|
+
target: formTarget || undefined,
|
|
49
|
+
priority: formPriority,
|
|
50
|
+
});
|
|
51
|
+
resetForm();
|
|
52
|
+
onRefresh();
|
|
53
|
+
} catch (e: any) {
|
|
54
|
+
formError = e.message;
|
|
55
|
+
} finally {
|
|
56
|
+
loading = false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function handleDelete(id: string) {
|
|
61
|
+
try {
|
|
62
|
+
actionError = null;
|
|
63
|
+
loading = true;
|
|
64
|
+
await deleteRoute(id);
|
|
65
|
+
onRefresh();
|
|
66
|
+
} catch (e: any) {
|
|
67
|
+
actionError = e.message;
|
|
68
|
+
} finally {
|
|
69
|
+
loading = false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<Card.Root>
|
|
75
|
+
<Card.Header class="pb-2 flex flex-row items-center justify-between">
|
|
76
|
+
<Card.Title class="text-sm font-medium">Routes</Card.Title>
|
|
77
|
+
<Button variant="outline" size="sm" onclick={() => (showForm = !showForm)}>
|
|
78
|
+
{showForm ? 'Cancel' : '+ New Route'}
|
|
79
|
+
</Button>
|
|
80
|
+
</Card.Header>
|
|
81
|
+
<Card.Content>
|
|
82
|
+
{#if actionError}
|
|
83
|
+
<div class="rounded-md bg-destructive/10 p-2 text-xs text-destructive mb-2">{actionError}</div>
|
|
84
|
+
{/if}
|
|
85
|
+
|
|
86
|
+
{#if showForm}
|
|
87
|
+
<div class="rounded-md border p-3 mb-4 space-y-3">
|
|
88
|
+
{#if formError}
|
|
89
|
+
<div class="rounded-md bg-destructive/10 p-2 text-xs text-destructive">{formError}</div>
|
|
90
|
+
{/if}
|
|
91
|
+
<div class="space-y-1">
|
|
92
|
+
<label class="text-xs text-muted-foreground" for="route-pattern">Pattern</label>
|
|
93
|
+
<input
|
|
94
|
+
id="route-pattern"
|
|
95
|
+
type="text"
|
|
96
|
+
bind:value={formPattern}
|
|
97
|
+
class="w-full rounded-md border border-input bg-background px-3 py-1.5 text-sm"
|
|
98
|
+
placeholder="agent.events.>"
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="grid grid-cols-2 gap-3">
|
|
102
|
+
<div class="space-y-1">
|
|
103
|
+
<label class="text-xs text-muted-foreground" for="route-target">Target</label>
|
|
104
|
+
<input
|
|
105
|
+
id="route-target"
|
|
106
|
+
type="text"
|
|
107
|
+
bind:value={formTarget}
|
|
108
|
+
class="w-full rounded-md border border-input bg-background px-3 py-1.5 text-sm"
|
|
109
|
+
placeholder="main"
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
<div class="space-y-1">
|
|
113
|
+
<label class="text-xs text-muted-foreground" for="route-priority">Priority</label>
|
|
114
|
+
<input
|
|
115
|
+
id="route-priority"
|
|
116
|
+
type="number"
|
|
117
|
+
min="1"
|
|
118
|
+
max="10"
|
|
119
|
+
bind:value={formPriority}
|
|
120
|
+
class="w-full rounded-md border border-input bg-background px-3 py-1.5 text-sm"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
<div class="flex gap-2">
|
|
125
|
+
<Button size="sm" onclick={handleCreate} disabled={loading}>
|
|
126
|
+
{loading ? 'Creating...' : 'Create'}
|
|
127
|
+
</Button>
|
|
128
|
+
<Button variant="ghost" size="sm" onclick={resetForm}>Cancel</Button>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
{/if}
|
|
132
|
+
|
|
133
|
+
{#if routes.length === 0}
|
|
134
|
+
<p class="text-sm text-muted-foreground py-4">No routes configured</p>
|
|
135
|
+
{:else}
|
|
136
|
+
<Table.Root>
|
|
137
|
+
<Table.Header>
|
|
138
|
+
<Table.Row>
|
|
139
|
+
<Table.Head>Pattern</Table.Head>
|
|
140
|
+
<Table.Head>Target</Table.Head>
|
|
141
|
+
<Table.Head>Priority</Table.Head>
|
|
142
|
+
<Table.Head>Enabled</Table.Head>
|
|
143
|
+
<Table.Head>Deliveries</Table.Head>
|
|
144
|
+
<Table.Head>Last Delivered</Table.Head>
|
|
145
|
+
<Table.Head>Lag</Table.Head>
|
|
146
|
+
<Table.Head class="w-16"></Table.Head>
|
|
147
|
+
</Table.Row>
|
|
148
|
+
</Table.Header>
|
|
149
|
+
<Table.Body>
|
|
150
|
+
{#each routes as route}
|
|
151
|
+
<Table.Row>
|
|
152
|
+
<Table.Cell class="font-mono text-xs">{route.pattern}</Table.Cell>
|
|
153
|
+
<Table.Cell>{route.target}</Table.Cell>
|
|
154
|
+
<Table.Cell>
|
|
155
|
+
<Badge variant={priorityVariant(route.priority)}>{route.priority}</Badge>
|
|
156
|
+
</Table.Cell>
|
|
157
|
+
<Table.Cell>
|
|
158
|
+
<Badge variant={route.enabled ? 'default' : 'secondary'}>
|
|
159
|
+
{route.enabled ? 'on' : 'off'}
|
|
160
|
+
</Badge>
|
|
161
|
+
</Table.Cell>
|
|
162
|
+
<Table.Cell>{route.deliveryCount}</Table.Cell>
|
|
163
|
+
<Table.Cell class="text-xs text-muted-foreground">
|
|
164
|
+
{route.lastDeliveredAt ? relativeAge(new Date(route.lastDeliveredAt).getTime()) : '\u2014'}
|
|
165
|
+
</Table.Cell>
|
|
166
|
+
<Table.Cell class="text-xs text-muted-foreground">
|
|
167
|
+
{formatDuration(route.lagMs)}
|
|
168
|
+
</Table.Cell>
|
|
169
|
+
<Table.Cell>
|
|
170
|
+
<Button variant="ghost" size="sm" onclick={() => handleDelete(route.id)}>Delete</Button>
|
|
171
|
+
</Table.Cell>
|
|
172
|
+
</Table.Row>
|
|
173
|
+
{/each}
|
|
174
|
+
</Table.Body>
|
|
175
|
+
</Table.Root>
|
|
176
|
+
{/if}
|
|
177
|
+
</Card.Content>
|
|
178
|
+
</Card.Root>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount } from 'svelte';
|
|
3
|
+
import Monitor from '@lucide/svelte/icons/monitor';
|
|
4
|
+
import Moon from '@lucide/svelte/icons/moon';
|
|
5
|
+
import Sun from '@lucide/svelte/icons/sun';
|
|
6
|
+
|
|
7
|
+
type Theme = 'system' | 'dark' | 'light';
|
|
8
|
+
|
|
9
|
+
let theme: Theme = $state('system');
|
|
10
|
+
|
|
11
|
+
const modes: { id: Theme; icon: typeof Monitor; label: string }[] = [
|
|
12
|
+
{ id: 'system', icon: Monitor, label: 'System' },
|
|
13
|
+
{ id: 'dark', icon: Moon, label: 'Dark' },
|
|
14
|
+
{ id: 'light', icon: Sun, label: 'Light' },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function apply(t: Theme) {
|
|
18
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
19
|
+
const isDark = t === 'dark' || (t === 'system' && prefersDark);
|
|
20
|
+
document.documentElement.classList.toggle('dark', isDark);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function setTheme(t: Theme) {
|
|
24
|
+
theme = t;
|
|
25
|
+
localStorage.setItem('nats-theme', t);
|
|
26
|
+
apply(t);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
onMount(() => {
|
|
30
|
+
const saved = localStorage.getItem('nats-theme') as Theme | null;
|
|
31
|
+
theme = saved && ['system', 'dark', 'light'].includes(saved) ? saved : 'system';
|
|
32
|
+
apply(theme);
|
|
33
|
+
|
|
34
|
+
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
|
35
|
+
const onChange = () => { if (theme === 'system') apply('system'); };
|
|
36
|
+
mql.addEventListener('change', onChange);
|
|
37
|
+
return () => mql.removeEventListener('change', onChange);
|
|
38
|
+
});
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<div class="flex items-center gap-0.5 rounded-lg bg-secondary p-0.5">
|
|
42
|
+
{#each modes as mode}
|
|
43
|
+
<button
|
|
44
|
+
class="inline-flex items-center justify-center rounded-md p-1.5 transition-colors
|
|
45
|
+
{theme === mode.id
|
|
46
|
+
? 'bg-accent text-foreground shadow-sm'
|
|
47
|
+
: 'text-muted-foreground hover:text-foreground'}"
|
|
48
|
+
onclick={() => setTheme(mode.id)}
|
|
49
|
+
title={mode.label}
|
|
50
|
+
>
|
|
51
|
+
<mode.icon size={14} />
|
|
52
|
+
</button>
|
|
53
|
+
{/each}
|
|
54
|
+
</div>
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const BASE = import.meta.env.BASE_URL + 'api';
|
|
2
|
+
|
|
3
|
+
async function fetchJSON<T>(path: string, init?: RequestInit): Promise<T> {
|
|
4
|
+
const res = await fetch(`${BASE}${path}`, init);
|
|
5
|
+
if (!res.ok) throw new Error(`API error: ${res.status}`);
|
|
6
|
+
const json = await res.json();
|
|
7
|
+
return json.result ?? json;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface HealthStatus {
|
|
11
|
+
nats: { connected: boolean; url: string };
|
|
12
|
+
gateway: { connected: boolean; url: string };
|
|
13
|
+
pendingCount: number;
|
|
14
|
+
uptimeSeconds: number;
|
|
15
|
+
config: {
|
|
16
|
+
streams: string[];
|
|
17
|
+
consumerName: string;
|
|
18
|
+
dedupTtlSeconds: number;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PendingEvent {
|
|
23
|
+
id: string;
|
|
24
|
+
sessionKey: string;
|
|
25
|
+
subject: string;
|
|
26
|
+
payload: unknown;
|
|
27
|
+
priority: number;
|
|
28
|
+
createdAt: number;
|
|
29
|
+
deliveredAt: number | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function getHealth(): Promise<HealthStatus> {
|
|
33
|
+
return fetchJSON('/health');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function getPending(sessionKey: string): Promise<PendingEvent[]> {
|
|
37
|
+
return fetchJSON(`/pending/${encodeURIComponent(sessionKey)}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function markDelivered(ids: string[]): Promise<void> {
|
|
41
|
+
await fetchJSON('/pending/mark-delivered', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({ ids }),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Routes ──────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export interface EventRoute {
|
|
51
|
+
id: string;
|
|
52
|
+
pattern: string;
|
|
53
|
+
target: string;
|
|
54
|
+
priority: number;
|
|
55
|
+
enabled: boolean;
|
|
56
|
+
deliveryCount: number;
|
|
57
|
+
lastDeliveredAt: string | null;
|
|
58
|
+
lastEventSubject: string | null;
|
|
59
|
+
lagMs: number | null;
|
|
60
|
+
createdAt: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function getRoutes(): Promise<EventRoute[]> {
|
|
64
|
+
return fetchJSON('/routes/health');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function createRoute(body: {
|
|
68
|
+
pattern: string;
|
|
69
|
+
target?: string;
|
|
70
|
+
priority?: number;
|
|
71
|
+
}): Promise<EventRoute> {
|
|
72
|
+
return fetchJSON('/routes', {
|
|
73
|
+
method: 'POST',
|
|
74
|
+
headers: { 'Content-Type': 'application/json' },
|
|
75
|
+
body: JSON.stringify(body),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function deleteRoute(id: string): Promise<void> {
|
|
80
|
+
await fetchJSON(`/routes/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Cron Jobs ───────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export interface CronJob {
|
|
86
|
+
id: string;
|
|
87
|
+
name: string;
|
|
88
|
+
expr: string;
|
|
89
|
+
subject: string;
|
|
90
|
+
payload: unknown;
|
|
91
|
+
timezone: string;
|
|
92
|
+
enabled: boolean;
|
|
93
|
+
lastRunAt: number | null;
|
|
94
|
+
createdAt: number;
|
|
95
|
+
nextRun: string | null;
|
|
96
|
+
isRunning: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function getCronJobs(): Promise<CronJob[]> {
|
|
100
|
+
return fetchJSON('/cron');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function createCronJob(body: {
|
|
104
|
+
name: string;
|
|
105
|
+
cron: string;
|
|
106
|
+
subject: string;
|
|
107
|
+
payload?: unknown;
|
|
108
|
+
timezone?: string;
|
|
109
|
+
}): Promise<CronJob> {
|
|
110
|
+
return fetchJSON('/cron', {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
body: JSON.stringify(body),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function deleteCronJob(name: string): Promise<void> {
|
|
118
|
+
await fetchJSON(`/cron/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function toggleCronJob(name: string): Promise<CronJob> {
|
|
122
|
+
return fetchJSON(`/cron/${encodeURIComponent(name)}/toggle`, { method: 'PATCH' });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function runCronJobNow(name: string): Promise<void> {
|
|
126
|
+
await fetchJSON(`/cron/${encodeURIComponent(name)}/run`, { method: 'POST' });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── Metrics ─────────────────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
export interface SubjectMetric {
|
|
132
|
+
subject: string;
|
|
133
|
+
published: number;
|
|
134
|
+
consumed: number;
|
|
135
|
+
lastPublishedAt: number | null;
|
|
136
|
+
lastConsumedAt: number | null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function getMetrics(): Promise<SubjectMetric[]> {
|
|
140
|
+
return fetchJSON('/metrics');
|
|
141
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { type VariantProps, tv } from "tailwind-variants";
|
|
3
|
+
|
|
4
|
+
export const badgeVariants = tv({
|
|
5
|
+
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
|
|
6
|
+
variants: {
|
|
7
|
+
variant: {
|
|
8
|
+
default:
|
|
9
|
+
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
|
|
10
|
+
secondary:
|
|
11
|
+
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
|
12
|
+
destructive:
|
|
13
|
+
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/70 border-transparent text-white",
|
|
14
|
+
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
variant: "default",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<script lang="ts">
|
|
26
|
+
import type { HTMLAnchorAttributes } from "svelte/elements";
|
|
27
|
+
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
ref = $bindable(null),
|
|
31
|
+
href,
|
|
32
|
+
class: className,
|
|
33
|
+
variant = "default",
|
|
34
|
+
children,
|
|
35
|
+
...restProps
|
|
36
|
+
}: WithElementRef<HTMLAnchorAttributes> & {
|
|
37
|
+
variant?: BadgeVariant;
|
|
38
|
+
} = $props();
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<svelte:element
|
|
42
|
+
this={href ? "a" : "span"}
|
|
43
|
+
bind:this={ref}
|
|
44
|
+
data-slot="badge"
|
|
45
|
+
{href}
|
|
46
|
+
class={cn(badgeVariants({ variant }), className)}
|
|
47
|
+
{...restProps}
|
|
48
|
+
>
|
|
49
|
+
{@render children?.()}
|
|
50
|
+
</svelte:element>
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
3
|
+
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from "svelte/elements";
|
|
4
|
+
import { type VariantProps, tv } from "tailwind-variants";
|
|
5
|
+
|
|
6
|
+
export const buttonVariants = tv({
|
|
7
|
+
base: "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs",
|
|
11
|
+
destructive:
|
|
12
|
+
"bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 text-white shadow-xs",
|
|
13
|
+
outline:
|
|
14
|
+
"bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 border shadow-xs",
|
|
15
|
+
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-xs",
|
|
16
|
+
ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
17
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
18
|
+
},
|
|
19
|
+
size: {
|
|
20
|
+
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
|
21
|
+
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
|
|
22
|
+
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
|
23
|
+
icon: "size-9",
|
|
24
|
+
"icon-sm": "size-8",
|
|
25
|
+
"icon-lg": "size-10",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
variant: "default",
|
|
30
|
+
size: "default",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
|
35
|
+
export type ButtonSize = VariantProps<typeof buttonVariants>["size"];
|
|
36
|
+
|
|
37
|
+
export type ButtonProps = WithElementRef<HTMLButtonAttributes> &
|
|
38
|
+
WithElementRef<HTMLAnchorAttributes> & {
|
|
39
|
+
variant?: ButtonVariant;
|
|
40
|
+
size?: ButtonSize;
|
|
41
|
+
};
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<script lang="ts">
|
|
45
|
+
let {
|
|
46
|
+
class: className,
|
|
47
|
+
variant = "default",
|
|
48
|
+
size = "default",
|
|
49
|
+
ref = $bindable(null),
|
|
50
|
+
href = undefined,
|
|
51
|
+
type = "button",
|
|
52
|
+
disabled,
|
|
53
|
+
children,
|
|
54
|
+
...restProps
|
|
55
|
+
}: ButtonProps = $props();
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
{#if href}
|
|
59
|
+
<a
|
|
60
|
+
bind:this={ref}
|
|
61
|
+
data-slot="button"
|
|
62
|
+
class={cn(buttonVariants({ variant, size }), className)}
|
|
63
|
+
href={disabled ? undefined : href}
|
|
64
|
+
aria-disabled={disabled}
|
|
65
|
+
role={disabled ? "link" : undefined}
|
|
66
|
+
tabindex={disabled ? -1 : undefined}
|
|
67
|
+
{...restProps}
|
|
68
|
+
>
|
|
69
|
+
{@render children?.()}
|
|
70
|
+
</a>
|
|
71
|
+
{:else}
|
|
72
|
+
<button
|
|
73
|
+
bind:this={ref}
|
|
74
|
+
data-slot="button"
|
|
75
|
+
class={cn(buttonVariants({ variant, size }), className)}
|
|
76
|
+
{type}
|
|
77
|
+
{disabled}
|
|
78
|
+
{...restProps}
|
|
79
|
+
>
|
|
80
|
+
{@render children?.()}
|
|
81
|
+
</button>
|
|
82
|
+
{/if}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import Root, {
|
|
2
|
+
type ButtonProps,
|
|
3
|
+
type ButtonSize,
|
|
4
|
+
type ButtonVariant,
|
|
5
|
+
buttonVariants,
|
|
6
|
+
} from "./button.svelte";
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
Root,
|
|
10
|
+
type ButtonProps as Props,
|
|
11
|
+
//
|
|
12
|
+
Root as Button,
|
|
13
|
+
buttonVariants,
|
|
14
|
+
type ButtonProps,
|
|
15
|
+
type ButtonSize,
|
|
16
|
+
type ButtonVariant,
|
|
17
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
class: className,
|
|
8
|
+
children,
|
|
9
|
+
...restProps
|
|
10
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div
|
|
14
|
+
bind:this={ref}
|
|
15
|
+
data-slot="card-action"
|
|
16
|
+
class={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
|
|
17
|
+
{...restProps}
|
|
18
|
+
>
|
|
19
|
+
{@render children?.()}
|
|
20
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
class: className,
|
|
8
|
+
children,
|
|
9
|
+
...restProps
|
|
10
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div bind:this={ref} data-slot="card-content" class={cn("px-6", className)} {...restProps}>
|
|
14
|
+
{@render children?.()}
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
class: className,
|
|
8
|
+
children,
|
|
9
|
+
...restProps
|
|
10
|
+
}: WithElementRef<HTMLAttributes<HTMLParagraphElement>> = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<p
|
|
14
|
+
bind:this={ref}
|
|
15
|
+
data-slot="card-description"
|
|
16
|
+
class={cn("text-muted-foreground text-sm", className)}
|
|
17
|
+
{...restProps}
|
|
18
|
+
>
|
|
19
|
+
{@render children?.()}
|
|
20
|
+
</p>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn, type WithElementRef } from "$lib/utils.js";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
class: className,
|
|
8
|
+
children,
|
|
9
|
+
...restProps
|
|
10
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<div
|
|
14
|
+
bind:this={ref}
|
|
15
|
+
data-slot="card-footer"
|
|
16
|
+
class={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
|
17
|
+
{...restProps}
|
|
18
|
+
>
|
|
19
|
+
{@render children?.()}
|
|
20
|
+
</div>
|