feat: silent background polling for dashboard and briefing views

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 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 17:43:21 -04:00
parent a63e498067
commit 08d738ddfb
2 changed files with 62 additions and 0 deletions
+22
View File
@@ -189,9 +189,31 @@ function toMsg(m: BriefingMessage): Message {
}
}
// ─── Background refresh (no-flicker) ─────────────────────────────────────────
let _refreshTimer: ReturnType<typeof setInterval> | 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)
})
</script>
+40
View File
@@ -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<typeof setInterval> | 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<TaskListResponse>('/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<TaskListResponse>(
`/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();