diff --git a/dashboard/package.json b/dashboard/package.json index 95fa9a1..6d38b84 100644 --- a/dashboard/package.json +++ b/dashboard/package.json @@ -1,6 +1,6 @@ { "name": "picloud-dashboard", - "version": "0.14.0", + "version": "0.15.0", "private": true, "type": "module", "scripts": { diff --git a/dashboard/src/lib/api.ts b/dashboard/src/lib/api.ts index f5377f4..31ed8a1 100644 --- a/dashboard/src/lib/api.ts +++ b/dashboard/src/lib/api.ts @@ -218,7 +218,8 @@ export type TriggerKind = | 'cron' | 'files' | 'pubsub' - | 'email'; + | 'email' + | 'queue'; export type TriggerDispatchMode = 'sync' | 'async'; /// Per-kind detail, tagged by `kind` to match the Rust serde shape. @@ -229,7 +230,8 @@ export type TriggerDetails = | { kind: 'cron'; schedule: string; timezone: string; last_fired_at?: string | null } | { kind: 'files'; collection_glob: string; ops: string[] } | { kind: 'pubsub'; topic_pattern: string } - | { kind: 'email'; has_inbound_secret: boolean }; + | { kind: 'email'; has_inbound_secret: boolean } + | { kind: 'queue'; queue_name: string; visibility_timeout_secs: number; last_fired_at?: string | null }; export interface CreateEmailTriggerInput { script_id: string; @@ -347,6 +349,37 @@ export interface CreatePubsubTriggerInput { retry_base_ms?: number; } +// v1.1.9 — queue:receive trigger. +export interface CreateQueueTriggerInput { + script_id: string; + queue_name: string; + visibility_timeout_secs?: number; + dispatch_mode?: TriggerDispatchMode; + retry_max_attempts?: number; + retry_backoff?: 'exponential' | 'linear' | 'constant'; + retry_base_ms?: number; +} + +// v1.1.9 — read-only queue inspection types (admin queues API). +export interface QueueSummary { + queue_name: string; + total: number; + pending: number; + claimed: number; +} + +export interface QueueConsumer { + trigger_id: string; + script_id: string; + script_name: string; + visibility_timeout_secs: number; + last_fired_at: string | null; +} + +export interface QueueDetail extends QueueSummary { + consumer: QueueConsumer | null; +} + // v1.1.6 — externally-subscribable realtime topics. export type TopicAuthMode = 'public' | 'token' | 'session'; @@ -755,6 +788,11 @@ export const api = { `/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/triggers/email`, { method: 'POST', body: JSON.stringify(input) } ), + createQueue: (idOrSlug: string, input: CreateQueueTriggerInput) => + adminRequest( + `/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/triggers/queue`, + { method: 'POST', body: JSON.stringify(input) } + ), remove: (idOrSlug: string, triggerId: string) => adminRequest( `/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/triggers/${triggerId}`, @@ -762,6 +800,17 @@ export const api = { ) }, + queues: { + list: (idOrSlug: string) => + adminRequest( + `/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/queues` + ), + get: (idOrSlug: string, name: string) => + adminRequest( + `/api/v1/admin/apps/${encodeURIComponent(idOrSlug)}/queues/${encodeURIComponent(name)}` + ) + }, + topics: { list: (idOrSlug: string) => adminRequest<{ topics: Topic[] }>( diff --git a/dashboard/src/routes/apps/[slug]/+page.svelte b/dashboard/src/routes/apps/[slug]/+page.svelte index 9423cc4..b54655f 100644 --- a/dashboard/src/routes/apps/[slug]/+page.svelte +++ b/dashboard/src/routes/apps/[slug]/+page.svelte @@ -131,6 +131,13 @@ let createEmailSecret = $state(''); let creatingEmail = $state(false); let createEmailError = $state(null); + // Queue triggers (v1.1.9). + let createQueueScriptId = $state(''); + let createQueueName = $state(''); + let createQueueVisibilityTimeout = $state(30); + let createQueueMaxAttempts = $state(3); + let creatingQueue = $state(false); + let createQueueError = $state(null); let triggerToRemove = $state(null); let removingTrigger = $state(false); // Endpoint scripts only — modules can't be trigger targets. @@ -208,6 +215,31 @@ } } + async function submitCreateQueue(e: SubmitEvent) { + e.preventDefault(); + if (!app) return; + creatingQueue = true; + createQueueError = null; + try { + await api.triggers.createQueue(app.id, { + script_id: createQueueScriptId, + queue_name: createQueueName.trim(), + visibility_timeout_secs: createQueueVisibilityTimeout, + retry_max_attempts: createQueueMaxAttempts + }); + createQueueScriptId = ''; + createQueueName = ''; + createQueueVisibilityTimeout = 30; + createQueueMaxAttempts = 3; + await loadTriggers(app.id); + } catch (err) { + createQueueError = + err instanceof ApiError ? err.message : err instanceof Error ? err.message : String(err); + } finally { + creatingQueue = false; + } + } + // The inbound-email webhook URL for a given email trigger (shown so // the operator can configure their provider). function emailInboundUrl(triggerId: string): string { @@ -797,6 +829,13 @@ > Files + + Queues + +

Queue:receive trigger (v1.1.9)

+

+ Register a script as the consumer for a per-app durable queue. Exactly + one consumer per queue is allowed — the registration will be rejected + if another consumer already exists. Use the + Queues tab for a read-only view + of message depths. +

+
+
+ + +
+
+ + +
+ {#if createQueueError} +
{createQueueError}
+ {/if} +
+ +
+
+ {#if triggers.length === 0}

No triggers in this app yet.

{:else} @@ -1216,6 +1319,12 @@ {t.details.has_inbound_secret ? 'signed (HMAC)' : 'unsigned'} {emailInboundUrl(t.id)} + {:else if t.details.kind === 'queue'} + {t.details.queue_name} + — vt {t.details.visibility_timeout_secs}s + + last fired: {t.details.last_fired_at ?? 'never'} + {/if} → {t.script_id} diff --git a/dashboard/src/routes/apps/[slug]/queues/+page.svelte b/dashboard/src/routes/apps/[slug]/queues/+page.svelte new file mode 100644 index 0000000..3af7bd3 --- /dev/null +++ b/dashboard/src/routes/apps/[slug]/queues/+page.svelte @@ -0,0 +1,128 @@ + + + + Queues — {app?.name ?? slug} — PiCloud + + +
+ ← Back to {app?.name ?? slug} +

Queues

+

+ Per-app durable queues. Producers call queue::enqueue(name, msg); + consumers register a queue:receive trigger. v1.1.9. +

+
+ +{#if error} +

{error}

+{/if} + +{#if loading && queues.length === 0} +

Loading…

+{:else if queues.length === 0} +

+ No queues in this app yet. A queue is created implicitly the first time a + message is enqueued. Register a queue:receive trigger from the + Triggers tab to consume messages. +

+{:else} + + + + + + + + + + + + {#each queues as q (q.queue_name)} + + + + + + + + {/each} + +
QueueTotalPendingClaimed
{q.queue_name}{q.total}{q.pending}{q.claimed} + + drilldown → + +
+{/if} + + diff --git a/dashboard/src/routes/apps/[slug]/queues/[name]/+page.svelte b/dashboard/src/routes/apps/[slug]/queues/[name]/+page.svelte new file mode 100644 index 0000000..6f0bc2c --- /dev/null +++ b/dashboard/src/routes/apps/[slug]/queues/[name]/+page.svelte @@ -0,0 +1,123 @@ + + + + {name} — Queues — {app?.name ?? slug} — PiCloud + + +
+ ← Back to Queues +

Queue: {name}

+
+ +{#if error} +

{error}

+{/if} + +{#if loading && !detail} +

Loading…

+{:else if detail} +
+

Depth

+
+
Total
+
{detail.total}
+
Pending
+
{detail.pending}
+
Claimed
+
{detail.claimed}
+
+
+ +
+

Consumer

+ {#if detail.consumer} +
+
Script
+
+ + {detail.consumer.script_name} + +
+
Visibility timeout
+
{detail.consumer.visibility_timeout_secs}s
+
Last fired
+
{fmtTime(detail.consumer.last_fired_at)}
+
Trigger ID
+
{detail.consumer.trigger_id}
+
+ {:else} +

+ No queue:receive trigger registered for this queue. Messages + enqueued here will accumulate without a consumer. Register one in the + Triggers tab of the parent app page. +

+ {/if} +
+{/if} + +