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:
@@ -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 () => {
|
onMounted(async () => {
|
||||||
await checkSetup()
|
await checkSetup()
|
||||||
if (!showWizard.value) await loadAll()
|
if (!showWizard.value) await loadAll()
|
||||||
|
_refreshTimer = setInterval(_backgroundRefreshMessages, 60_000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (_refreshTimer !== null) clearInterval(_refreshTimer)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,43 @@ function milestoneColor(index: number): string {
|
|||||||
return palette[index % palette.length];
|
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 ─────────────────────────────────────────────────────────────
|
// ─── Data loading ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -135,6 +172,8 @@ onMounted(async () => {
|
|||||||
// Focus chat input after data loads
|
// Focus chat input after data loads
|
||||||
chatInputRef.value?.focus();
|
chatInputRef.value?.focus();
|
||||||
loadProjects();
|
loadProjects();
|
||||||
|
|
||||||
|
_refreshTimer = setInterval(_backgroundRefresh, 90_000)
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadProjects() {
|
async function loadProjects() {
|
||||||
@@ -200,6 +239,7 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
document.removeEventListener("shortcut:focus-chat", onFocusChatShortcut);
|
document.removeEventListener("shortcut:focus-chat", onFocusChatShortcut);
|
||||||
|
if (_refreshTimer !== null) clearInterval(_refreshTimer)
|
||||||
});
|
});
|
||||||
|
|
||||||
const chatStore = useChatStore();
|
const chatStore = useChatStore();
|
||||||
|
|||||||
Reference in New Issue
Block a user