summaryrefslogtreecommitdiffhomepage
path: root/src/app
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-27 00:03:21 +0900
committerAdam Malczewski <[email protected]>2026-06-27 00:03:21 +0900
commit80f99665034a0e510300793205c162fc7a46769f (patch)
treefcff9e54903d107449b29b77a0ac6f70094e100b /src/app
parent121fd7280bc3f77c61da7025cca06480798038b7 (diff)
downloaddispatch-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.svelte6
-rw-r--r--src/app/store.svelte.ts39
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);
},