feat(v1.1.9): dashboard Queues tab + queue:receive trigger form + 0.15.0

API client (src/lib/api.ts):
- TriggerKind gains 'queue'
- TriggerDetails gains { kind: 'queue', queue_name, visibility_timeout_secs, last_fired_at }
- CreateQueueTriggerInput, QueueSummary, QueueConsumer, QueueDetail
- api.triggers.createQueue(idOrSlug, input) -> POST /admin/.../triggers/queue
- api.queues.list(idOrSlug) -> GET /admin/.../queues
- api.queues.get(idOrSlug, name) -> GET /admin/.../queues/{name}

New routes:
- /apps/[slug]/queues — read-only list view (queue_name, total, pending,
  claimed, drilldown link); empty state explains how queues are created
  (first enqueue) and that consumers register via the Triggers tab
- /apps/[slug]/queues/[name] — drilldown showing depth + registered
  consumer (script name + visibility timeout + last_fired_at + trigger
  id); empty consumer state surfaces clearly

App detail page (src/routes/apps/[slug]/+page.svelte):
- New "Queues" link in the app-level nav between Files and Dead letters
- Triggers tab gains a "Queue:receive trigger (v1.1.9)" form alongside
  cron/pubsub/email — target script select, queue_name, visibility
  timeout (5–3600s, default 30), max_attempts (1–20, default 3)
- Trigger list renders queue triggers with queue_name + visibility
  timeout + last_fired_at

dashboard/package.json: 0.14.0 -> 0.15.0

npm run check — all queue/invoke-related code typechecks clean; the
existing Playwright test errors (149 unrelated) carry forward unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-06 22:41:59 +02:00
parent 714f6dae18
commit 529d34af83
5 changed files with 412 additions and 3 deletions

View File

@@ -131,6 +131,13 @@
let createEmailSecret = $state('');
let creatingEmail = $state(false);
let createEmailError = $state<string | null>(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<string | null>(null);
let triggerToRemove = $state<Trigger | null>(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
</a>
<a
class="tab-link"
href="{base}/apps/{slug}/queues"
title="Queues — depth of per-app durable queues (v1.1.9, read-only)"
>
Queues
</a>
<a
class="tab-link"
href="{base}/apps/{slug}/dead-letters"
@@ -1192,6 +1231,70 @@
</div>
</form>
<h3>Queue:receive trigger (v1.1.9)</h3>
<p class="muted">
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
<a href="{base}/apps/{slug}/queues">Queues</a> tab for a read-only view
of message depths.
</p>
<form class="create-form" onsubmit={submitCreateQueue}>
<div class="row">
<label>
<span>Target script</span>
<select bind:value={createQueueScriptId} required>
<option value="" disabled>Select an endpoint script…</option>
{#each endpointScripts as s (s.id)}
<option value={s.id}>{s.name}</option>
{/each}
</select>
</label>
<label class="grow">
<span>Queue name</span>
<input
type="text"
bind:value={createQueueName}
placeholder="payments.process"
required
/>
</label>
</div>
<div class="row">
<label>
<span>Visibility timeout (53600s)</span>
<input
type="number"
bind:value={createQueueVisibilityTimeout}
min="5"
max="3600"
required
/>
</label>
<label>
<span>Max attempts (120)</span>
<input
type="number"
bind:value={createQueueMaxAttempts}
min="1"
max="20"
required
/>
</label>
</div>
{#if createQueueError}
<div class="error">{createQueueError}</div>
{/if}
<div class="actions">
<button
type="submit"
disabled={creatingQueue || !createQueueScriptId || !createQueueName}
>
{creatingQueue ? 'Creating…' : 'Create queue:receive trigger'}
</button>
</div>
</form>
{#if triggers.length === 0}
<p class="muted">No triggers in this app yet.</p>
{:else}
@@ -1216,6 +1319,12 @@
{t.details.has_inbound_secret ? 'signed (HMAC)' : 'unsigned'}
</span>
<code class="webhook-url">{emailInboundUrl(t.id)}</code>
{:else if t.details.kind === 'queue'}
<code>{t.details.queue_name}</code>
<span class="muted">— vt {t.details.visibility_timeout_secs}s</span>
<span class="muted small">
last fired: {t.details.last_fired_at ?? 'never'}
</span>
{/if}
<span class="muted small">{t.script_id}</span>
</div>