diff --git a/src/lib/stores/completionStats.svelte.ts b/src/lib/stores/completionStats.svelte.ts new file mode 100644 index 0000000..68b786c --- /dev/null +++ b/src/lib/stores/completionStats.svelte.ts @@ -0,0 +1,56 @@ +// Phase 8 store for forgiving gamification. Owns the last-7-days series +// of completion counts rendered by the Tasks-page badge + sparkline. +// +// Refresh triggers: +// - module load (first time) +// - kon:task-completed (from page.svelte.ts completeTask) +// - kon:step-completed (from MicroSteps.svelte) +// - kon:task-uncompleted (new event, emitted by uncompleteTask in Task 10) +// - kon:task-deleted (new event, emitted by deleteTask in Task 10) +// - window focus (for day rollover while the app stayed open +// past midnight) +// +// We deliberately avoid polling. Every path that changes a row's +// done state or deletes a row emits one of the events above. + +import { invoke } from "@tauri-apps/api/core"; +import { hasTauriRuntime } from "$lib/utils/runtime.js"; +import type { DailyCompletionCount } from "$lib/types/app"; + +const DAYS = 7; + +export const recentCompletions = $state([]); + +// Reactive derived value. Svelte 5 template consumers read it as a +// plain property (no parens) and it recomputes when recentCompletions +// changes. +export const todayCount = $derived(recentCompletions.at(-1)?.count ?? 0); + +export async function refresh(): Promise { + if (!hasTauriRuntime()) return; + try { + const series = await invoke( + "list_recent_completions_cmd", + { days: DAYS }, + ); + recentCompletions.splice(0, recentCompletions.length, ...series); + } catch (err) { + // Do not toast. This is a background read. Log only. + console.error("completionStats.refresh failed", err); + } +} + +if (typeof window !== "undefined") { + const handler = () => { + refresh().catch(() => {}); + }; + window.addEventListener("kon:task-completed", handler); + window.addEventListener("kon:step-completed", handler); + window.addEventListener("kon:task-uncompleted", handler); + window.addEventListener("kon:task-deleted", handler); + window.addEventListener("focus", handler); + + if (hasTauriRuntime()) { + refresh().catch(() => {}); + } +}