diff options
| author | Adam Malczewski <[email protected]> | 2026-06-27 00:03:21 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-27 00:03:21 +0900 |
| commit | 80f99665034a0e510300793205c162fc7a46769f (patch) | |
| tree | fcff9e54903d107449b29b77a0ac6f70094e100b /src/app | |
| parent | 121fd7280bc3f77c61da7025cca06480798038b7 (diff) | |
| download | dispatch-web-80f99665034a0e510300793205c162fc7a46769f.tar.gz dispatch-web-80f99665034a0e510300793205c162fc7a46769f.zip | |
feat(heartbeat): show next heartbeat timer + backend handoff for timestamp endpoint
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/App.svelte | 6 | ||||
| -rw-r--r-- | src/app/store.svelte.ts | 39 |
2 files changed, 45 insertions, 0 deletions
diff --git a/src/app/App.svelte b/src/app/App.svelte index 7ad8733..09be947 100644 --- a/src/app/App.svelte +++ b/src/app/App.svelte @@ -64,6 +64,7 @@ manifest as heartbeatManifest, RunModal, type HeartbeatConfigResult, + type HeartbeatNextRunResult, type HeartbeatRunView, type HeartbeatRunsResult, type HeartbeatStopResult, @@ -399,6 +400,10 @@ return store.stopHeartbeatRun(runId); } + async function loadHeartbeatNextRun(): Promise<HeartbeatNextRunResult> { + return store.heartbeatNextRun(); + } + // Run-chat modal: open a live watch on the run's conversation (the store owns // the ChatStore + the `chat.subscribe` stream), and tear it down on close. function openRunChat(conversationId: string): ChatStore { @@ -682,6 +687,7 @@ stopRun={stopHeartbeatRun} loadVariables={loadSystemPromptVariablesPrompt} loadDefaultPrompt={loadSystemPromptPrompt} + loadNextRun={loadHeartbeatNextRun} onOpenRun={(run) => (heartbeatRun = run)} /> {/if} diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts index 1ad0c08..0e2d112 100644 --- a/src/app/store.svelte.ts +++ b/src/app/store.svelte.ts @@ -59,6 +59,7 @@ import type { HeartbeatConfig, HeartbeatConfigPatch, HeartbeatConfigResult, + HeartbeatNextRunResult, HeartbeatRun, HeartbeatRunsResult, HeartbeatStopResult, @@ -341,6 +342,15 @@ export interface AppStore { */ stopHeartbeatRun(runId: string): Promise<HeartbeatStopResult>; /** + * Fetch the server-authoritative next-run timestamp + * (`GET /workspaces/:id/heartbeat/next-run`) — when the next heartbeat run + * will fire (ISO 8601), or null when disabled / no run scheduled. The FE shows + * a live countdown from this. When the endpoint is absent (404 — backend + * hasn't shipped CR-HB-3 yet) it returns `ok: false` so the FE falls back to + * an approximation from the runs + config. + */ + heartbeatNextRun(): Promise<HeartbeatNextRunResult>; + /** * Open a "watch" on a conversation for a modal viewer (the heartbeat run-chat * modal): ensures a live {@link ChatStore} for the conversation, subscribing * to its turn stream (`chat.subscribe`) + loading history. Reuses the open @@ -1649,6 +1659,35 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { } }, + async heartbeatNextRun(): Promise<HeartbeatNextRunResult> { + const wsId = untrack(() => activeWorkspaceId); + try { + const res = await fetchImpl( + `${httpBase}/workspaces/${encodeURIComponent(wsId)}/heartbeat/next-run`, + ); + if (!res.ok) { + // 404 = the backend hasn't shipped CR-HB-3 yet → the FE falls back to + // an approximation. Surface as ok:false (non-fatal). + const errBody = (await res.json().catch(() => null)) as { error?: string } | null; + return { + ok: false, + error: errBody?.error ?? `Heartbeat next-run failed (HTTP ${res.status})`, + }; + } + const data = (await res.json().catch(() => null)) as { nextRunAt?: string | null } | null; + // `null` (disabled / no run scheduled) passes through; anything non-string + // also becomes null so a malformed body can't crash the countdown. + const raw = data?.nextRunAt; + const nextRunAt = typeof raw === "string" ? raw : null; + return { ok: true, nextRunAt }; + } catch (err) { + return { + ok: false, + error: err instanceof Error ? err.message : "Heartbeat next-run request failed", + }; + } + }, + watchConversation(conversationId: string): ChatStore { return watchConversation(conversationId); }, |
