feat(gamification): completionStats store

Owns the last-7-days completion series. Refreshes on task-completed /
step-completed / task-uncompleted / task-deleted window events and on
window focus (for day rollover). Derives todayCount from the newest
entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 20:38:18 +01:00
parent cb32285ce0
commit 4ffdae9838

View File

@@ -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<DailyCompletionCount[]>([]);
// 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<void> {
if (!hasTauriRuntime()) return;
try {
const series = await invoke<DailyCompletionCount[]>(
"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(() => {});
}
}