From 08d738ddfb553cb7c0bffd1b0fcecea3743166e3 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 26 Mar 2026 17:43:21 -0400 Subject: [PATCH] feat: silent background polling for dashboard and briefing views MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HomeView: setInterval every 90s refreshes events, orphan tasks/notes, and hero next-up task. Never touches loading ref, so the page content stays stable — only the data silently swaps when the fetch completes. Skips when document.hidden or initial load is still in progress. BriefingView: setInterval every 60s refetches today's messages, but only when viewing today's conversation and not currently streaming. Compares message count and last message content before updating to avoid unnecessary re-renders. Both timers are cleared on unmount. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/views/BriefingView.vue | 22 ++++++++++++++++ frontend/src/views/HomeView.vue | 40 +++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/frontend/src/views/BriefingView.vue b/frontend/src/views/BriefingView.vue index e8dee64..0be3cc6 100644 --- a/frontend/src/views/BriefingView.vue +++ b/frontend/src/views/BriefingView.vue @@ -189,9 +189,31 @@ function toMsg(m: BriefingMessage): Message { } } +// ─── Background refresh (no-flicker) ───────────────────────────────────────── +let _refreshTimer: ReturnType | null = null + +async function _backgroundRefreshMessages() { + if (document.hidden || chatStore.streaming || !isToday.value || !todayConvId.value) return + try { + const today = await getBriefingToday() + if (!today) return + const fresh = today.messages + const last = fresh[fresh.length - 1] + const cur = messages.value[messages.value.length - 1] + if (fresh.length !== messages.value.length || last?.content !== cur?.content) { + messages.value = fresh + } + } catch { /* silent — don't disturb the UI on network hiccup */ } +} + onMounted(async () => { await checkSetup() if (!showWizard.value) await loadAll() + _refreshTimer = setInterval(_backgroundRefreshMessages, 60_000) +}) + +onUnmounted(() => { + if (_refreshTimer !== null) clearInterval(_refreshTimer) }) diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 8be4ace..40664c3 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -82,6 +82,43 @@ function milestoneColor(index: number): string { return palette[index % palette.length]; } +// ─── Background refresh (no-flicker) ───────────────────────────────────────── +// Runs on a timer while the page is visible. Never touches `loading` so +// existing content stays on screen while the fetch is in flight. + +let _refreshTimer: ReturnType | null = null + +function _dateRange() { + const today = new Date() + const nextWeek = new Date(today) + nextWeek.setDate(today.getDate() + 7) + return { + todayStr: today.toISOString().slice(0, 10) + 'T00:00:00', + nextWeekStr: nextWeek.toISOString().slice(0, 10) + 'T23:59:59', + } +} + +function _backgroundRefresh() { + if (document.hidden || loading.value) return + const { todayStr, nextWeekStr } = _dateRange() + Promise.allSettled([ + listEvents(todayStr, nextWeekStr), + apiGet('/api/tasks?no_project=true&sort=updated_at&order=desc&limit=8'), + apiGet<{ notes: Note[] }>('/api/notes?type=note&no_project=true&sort=updated_at&order=desc&limit=6'), + ]).then(([eventsRes, tasksRes, notesRes]) => { + if (eventsRes.status === 'fulfilled') upcomingEvents.value = eventsRes.value + if (tasksRes.status === 'fulfilled') orphanTasks.value = tasksRes.value.tasks + if (notesRes.status === 'fulfilled') orphanNotes.value = notesRes.value.notes + }) + if (heroProject.value) { + apiGet( + `/api/tasks?project_id=${heroProject.value.id}&status=todo&sort=updated_at&order=desc&limit=1` + ) + .then((r) => { heroNextUp.value = r.tasks[0] ?? null }) + .catch(() => {}) + } +} + // ─── Data loading ───────────────────────────────────────────────────────────── onMounted(async () => { @@ -135,6 +172,8 @@ onMounted(async () => { // Focus chat input after data loads chatInputRef.value?.focus(); loadProjects(); + + _refreshTimer = setInterval(_backgroundRefresh, 90_000) }); async function loadProjects() { @@ -200,6 +239,7 @@ onMounted(() => { }); onUnmounted(() => { document.removeEventListener("shortcut:focus-chat", onFocusChatShortcut); + if (_refreshTimer !== null) clearInterval(_refreshTimer) }); const chatStore = useChatStore();