From 4dffeb941d6139db9bfd79eeef9fdf84ab70dbea Mon Sep 17 00:00:00 2001 From: The Product Creator Date: Fri, 20 Mar 2026 14:50:08 +0100 Subject: [PATCH 1/9] Add event tracking data layer Introduces useTracker composable that records overlay_shown, intention_kept, and unblock events to chrome.storage.local with a 90-day rolling window. Adds foqusEvents to storage defaults. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/composables/useStorage.js | 1 + src/composables/useTracker.js | 45 +++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/composables/useTracker.js diff --git a/src/composables/useStorage.js b/src/composables/useStorage.js index 20f1b47..fbed8ac 100644 --- a/src/composables/useStorage.js +++ b/src/composables/useStorage.js @@ -9,6 +9,7 @@ const DEFAULTS = { preferReducedMotion: false, darkMode: false, descriptionBannerDismissed: false, + foqusEvents: [], } /** diff --git a/src/composables/useTracker.js b/src/composables/useTracker.js new file mode 100644 index 0000000..ed322dd --- /dev/null +++ b/src/composables/useTracker.js @@ -0,0 +1,45 @@ +/** + * Event tracking for Foqus — append-only log stored in chrome.storage.local. + * Events are pruned to a 90-day rolling window on each write. + * + * Event shape: { type, host, ts, ?meta } + * Types: 'overlay_shown' | 'intention_kept' | 'unblock' + */ + +const STORAGE_KEY = 'foqusEvents' +const RETENTION_DAYS = 90 + +function pruneOldEvents(events) { + const cutoff = Date.now() - RETENTION_DAYS * 24 * 60 * 60 * 1000 + return events.filter((e) => e.ts >= cutoff) +} + +async function getEvents() { + const data = await chrome.storage.local.get(STORAGE_KEY) + return data[STORAGE_KEY] || [] +} + +async function recordEvent(type, host, meta) { + const events = await getEvents() + const event = { type, host, ts: Date.now() } + if (meta) event.meta = meta + events.push(event) + const pruned = pruneOldEvents(events) + await chrome.storage.local.set({ [STORAGE_KEY]: pruned }) + return event +} + +export function useTracker() { + return { + overlayShown(host) { + return recordEvent('overlay_shown', host) + }, + intentionKept(host) { + return recordEvent('intention_kept', host) + }, + unblock(host, minutes) { + return recordEvent('unblock', host, { minutes }) + }, + getEvents, + } +} From be6a33688737961b7517c93654cefe53f147c4c4 Mon Sep 17 00:00:00 2001 From: The Product Creator Date: Fri, 20 Mar 2026 14:50:13 +0100 Subject: [PATCH 2/9] Add stats engine for streaks and trends Introduces useStats composable that computes streak count, intentions kept (total + today), unblocks today, and weekly unblock trend from the event log. Includes getAllEventsForExport for data export. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/composables/useStats.js | 104 ++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 src/composables/useStats.js diff --git a/src/composables/useStats.js b/src/composables/useStats.js new file mode 100644 index 0000000..bb30116 --- /dev/null +++ b/src/composables/useStats.js @@ -0,0 +1,104 @@ +import { ref, onUnmounted } from 'vue' + +const STORAGE_KEY = 'foqusEvents' + +function toDateStr(ts) { + return new Date(ts).toLocaleDateString('en-CA') // YYYY-MM-DD +} + +function todayStr() { + return toDateStr(Date.now()) +} + +function yesterdayStr() { + return toDateStr(Date.now() - 24 * 60 * 60 * 1000) +} + +function computeStats(events) { + const now = Date.now() + const today = todayStr() + const weekAgo = now - 7 * 24 * 60 * 60 * 1000 + const twoWeeksAgo = now - 14 * 24 * 60 * 60 * 1000 + + // Intentions kept — total and today + const allKept = events.filter((e) => e.type === 'intention_kept') + const keptToday = allKept.filter((e) => toDateStr(e.ts) === today).length + const intentionsKept = allKept.length + + // Unblocks — total and today + const allUnblocks = events.filter((e) => e.type === 'unblock') + const unblocksToday = allUnblocks.filter((e) => toDateStr(e.ts) === today).length + + // Streak — consecutive days with at least one intention_kept + const keptDays = new Set(allKept.map((e) => toDateStr(e.ts))) + let streak = 0 + let checkDate = today + + // If no intention kept today, start checking from yesterday + // (streak is still alive if today isn't over yet) + if (!keptDays.has(today)) { + checkDate = yesterdayStr() + if (!keptDays.has(checkDate)) { + // No streak + return { intentionsKept, keptToday, unblocksToday, streak: 0, weeklyTrend: null } + } + } + + // Count backwards from checkDate + const d = new Date(checkDate + 'T12:00:00') + while (keptDays.has(toDateStr(d.getTime()))) { + streak++ + d.setDate(d.getDate() - 1) + } + + // Weekly trend — compare this week's unblocks to last week's + const thisWeekUnblocks = allUnblocks.filter((e) => e.ts >= weekAgo).length + const lastWeekUnblocks = allUnblocks.filter((e) => e.ts >= twoWeeksAgo && e.ts < weekAgo).length + let weeklyTrend = null + if (lastWeekUnblocks > 0) { + const change = Math.round(((thisWeekUnblocks - lastWeekUnblocks) / lastWeekUnblocks) * 100) + weeklyTrend = { thisWeek: thisWeekUnblocks, lastWeek: lastWeekUnblocks, change } + } + + return { intentionsKept, keptToday, unblocksToday, streak, weeklyTrend } +} + +/** + * Reactive stats computed from the event log. + * Auto-updates when chrome.storage changes. + */ +export function useStats() { + const stats = ref({ + intentionsKept: 0, + keptToday: 0, + unblocksToday: 0, + streak: 0, + weeklyTrend: null, + }) + + function load() { + chrome.storage.local.get(STORAGE_KEY, (data) => { + stats.value = computeStats(data[STORAGE_KEY] || []) + }) + } + + function listener(changes, areaName) { + if (areaName === 'local' && changes[STORAGE_KEY]) { + stats.value = computeStats(changes[STORAGE_KEY].newValue || []) + } + } + + load() + chrome.storage.onChanged.addListener(listener) + onUnmounted(() => chrome.storage.onChanged.removeListener(listener)) + + return { stats, refresh: load } +} + +/** + * Get all events for export — not reactive, just a one-shot read. + */ +export async function getAllEventsForExport() { + const data = await chrome.storage.local.get(STORAGE_KEY) + return data[STORAGE_KEY] || [] +} From 1cdb7338b0b5a6abffc4026a546b92e65058eadf Mon Sep 17 00:00:00 2001 From: The Product Creator Date: Fri, 20 Mar 2026 14:50:18 +0100 Subject: [PATCH 3/9] Wire event tracking into overlay and content script Records overlay_shown when overlay appears, intention_kept when user clicks a suggestion or navigates away, and unblock when user unblocks. Adds beforeunload listener to capture intention_kept on tab close. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/content/ContentApp.vue | 18 ++++++++++++++++++ src/content/Overlay.vue | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/content/ContentApp.vue b/src/content/ContentApp.vue index 25da542..34ed187 100644 --- a/src/content/ContentApp.vue +++ b/src/content/ContentApp.vue @@ -1,6 +1,7 @@ @@ -137,6 +154,7 @@ onMounted(() => { :prefer-reduced-motion="preferReducedMotion === true" :is-return="isReturn" @unblock="onUnblock" + @intention-kept="onIntentionKept" /> {

// go somewhere better