Skip to content

Commit 4fb4594

Browse files
committed
Complete MVP hardening, recovery, and full test loop
1 parent b1db2c7 commit 4fb4594

10 files changed

Lines changed: 787 additions & 93 deletions

File tree

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ Chrome extension MVP that tracks tab-level activity and shows what you worked on
3434
3. Run end-to-end tests:
3535
- `npm run test:e2e`
3636

37+
## Full Verification Loop
38+
39+
- Run unit and E2E suites together:
40+
- `npm run test:all`
41+
42+
Includes:
43+
- Unit tests for session engine transitions and IndexedDB retention/query boundaries.
44+
- Playwright dashboard tests for timeline filters, focus/open behavior, and settings action.
45+
3746
## Notes
3847

3948
- MVP tracks tab/window focus activity only.

background/service-worker.js

Lines changed: 52 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,26 @@ import {
55
pruneSessionsOlderThan,
66
upsertTabSnapshot
77
} from "../shared/db.js";
8-
import { toDurationSeconds } from "../shared/time.js";
9-
import { extractDomain, isExcludedDomain, isTrackableUrl, readableTitle } from "../shared/url.js";
8+
import { createSessionEngine } from "./session-engine.js";
9+
import { isExcludedDomain, isTrackableUrl, readableTitle } from "../shared/url.js";
1010

1111
const CLEANUP_ALARM = "retention-cleanup";
1212
const CLEANUP_INTERVAL_MINUTES = 360;
13-
const SWITCH_DEBOUNCE_MS = 250;
13+
const RUNTIME_STORAGE_KEY = "runtime_state_v1";
1414

1515
const state = {
16-
activeSession: null,
1716
focusedWindowId: chrome.windows.WINDOW_ID_NONE,
1817
idleState: "active",
1918
paused: false,
2019
excludedDomains: [],
21-
retentionDays: 30,
22-
lastContextFingerprint: "",
23-
lastContextAt: 0
20+
retentionDays: 30
2421
};
2522

23+
const sessionEngine = createSessionEngine({
24+
debounceMs: 250,
25+
isTrackableTab
26+
});
27+
2628
function getDashboardUrl() {
2729
return chrome.runtime.getURL("ui/dashboard.html");
2830
}
@@ -43,87 +45,59 @@ function isTrackableTab(tab) {
4345
return true;
4446
}
4547

46-
function createSession(tab) {
47-
const now = Date.now();
48-
return {
49-
id: crypto.randomUUID(),
50-
tabId: tab.id,
51-
windowId: tab.windowId,
52-
url: tab.url,
53-
title: readableTitle(tab.title, tab.url),
54-
domain: extractDomain(tab.url),
55-
startAt: now,
56-
endAt: now,
57-
durationSec: 0,
58-
endReason: "unknown"
59-
};
60-
}
61-
6248
async function refreshSettingsCache() {
6349
const settings = await getSettings();
6450
state.paused = Boolean(settings.paused);
6551
state.retentionDays = Number(settings.retentionDays) || 30;
6652
state.excludedDomains = Array.isArray(settings.excludedDomains) ? settings.excludedDomains : [];
6753
}
6854

69-
async function endActiveSession(reason) {
70-
if (!state.activeSession) {
71-
return;
72-
}
73-
74-
const now = Date.now();
75-
const completed = {
76-
...state.activeSession,
77-
endAt: now,
78-
durationSec: toDurationSeconds(state.activeSession.startAt, now),
79-
endReason: reason
80-
};
81-
82-
state.activeSession = null;
83-
state.lastContextFingerprint = "";
84-
state.lastContextAt = 0;
55+
function getActiveSession() {
56+
return sessionEngine.readState().activeSession;
57+
}
8558

86-
if (!completed.url || completed.durationSec < 0) {
59+
async function persistRuntimeState() {
60+
const runtimeState = sessionEngine.exportRuntimeState();
61+
if (runtimeState.activeSession) {
62+
await chrome.storage.local.set({
63+
[RUNTIME_STORAGE_KEY]: runtimeState
64+
});
8765
return;
8866
}
8967

90-
await addSession(completed);
68+
await chrome.storage.local.remove(RUNTIME_STORAGE_KEY);
9169
}
9270

93-
function isDebouncedContext(tab) {
94-
const fingerprint = `${tab.windowId}:${tab.id}:${tab.url}`;
95-
const now = Date.now();
96-
const isDebounced =
97-
fingerprint === state.lastContextFingerprint && now - state.lastContextAt < SWITCH_DEBOUNCE_MS;
71+
async function loadRuntimeState() {
72+
const stored = await chrome.storage.local.get(RUNTIME_STORAGE_KEY);
73+
const runtimeState = stored?.[RUNTIME_STORAGE_KEY];
74+
if (!runtimeState) {
75+
return false;
76+
}
9877

99-
state.lastContextFingerprint = fingerprint;
100-
state.lastContextAt = now;
101-
return isDebounced;
78+
return sessionEngine.hydrateRuntimeState(runtimeState);
10279
}
10380

104-
async function startOrSwitchSession(tab, reason) {
105-
if (!isTrackableTab(tab)) {
106-
await endActiveSession(reason);
81+
async function storeEndedSession(endedSession) {
82+
if (!endedSession || !endedSession.url || endedSession.durationSec < 0) {
10783
return;
10884
}
10985

110-
if (isDebouncedContext(tab)) {
111-
return;
112-
}
86+
await addSession(endedSession);
87+
}
11388

114-
if (
115-
state.activeSession &&
116-
state.activeSession.tabId === tab.id &&
117-
state.activeSession.windowId === tab.windowId
118-
) {
119-
state.activeSession.url = tab.url || state.activeSession.url;
120-
state.activeSession.title = readableTitle(tab.title, state.activeSession.url);
121-
state.activeSession.domain = extractDomain(state.activeSession.url);
122-
return;
123-
}
89+
async function endActiveSession(reason) {
90+
const endedSession = sessionEngine.endActiveSession(reason);
91+
await persistRuntimeState();
92+
await storeEndedSession(endedSession);
93+
return endedSession;
94+
}
12495

125-
await endActiveSession(reason);
126-
state.activeSession = createSession(tab);
96+
async function startOrSwitchSession(tab, reason) {
97+
const result = sessionEngine.startOrSwitchSession(tab, reason);
98+
await persistRuntimeState();
99+
await storeEndedSession(result.endedSession);
100+
return result;
127101
}
128102

129103
async function cacheTabSnapshot(tab) {
@@ -189,6 +163,7 @@ async function runRetentionCleanup() {
189163
async function initializeExtension(reason) {
190164
await initDatabase();
191165
await refreshSettingsCache();
166+
await loadRuntimeState();
192167

193168
await chrome.alarms.clear(CLEANUP_ALARM);
194169
await chrome.alarms.create(CLEANUP_ALARM, { periodInMinutes: CLEANUP_INTERVAL_MINUTES });
@@ -226,29 +201,23 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
226201
console.error("Failed to cache tab snapshot", error);
227202
});
228203

229-
if (
230-
state.activeSession &&
231-
tabId === state.activeSession.tabId &&
232-
typeof changeInfo.url === "string" &&
233-
isTrackableUrl(changeInfo.url)
234-
) {
235-
state.activeSession.url = changeInfo.url;
236-
state.activeSession.domain = extractDomain(changeInfo.url);
237-
}
238-
239-
if (state.activeSession && tabId === state.activeSession.tabId && typeof changeInfo.title === "string") {
240-
state.activeSession.title = readableTitle(changeInfo.title, state.activeSession.url);
204+
const activeSessionChanged = sessionEngine.updateActiveSessionMetadata(tabId, changeInfo);
205+
if (activeSessionChanged) {
206+
persistRuntimeState().catch((error) => {
207+
console.error("Failed to persist runtime state on metadata update", error);
208+
});
241209
}
242210

243-
if (tab.active && tab.windowId === state.focusedWindowId && (changeInfo.url || changeInfo.status === "complete")) {
211+
if (tab && tab.active && tab.windowId === state.focusedWindowId && (changeInfo.url || changeInfo.status === "complete")) {
244212
syncCurrentActiveContext("navigation").catch((error) => {
245213
console.error("Failed to sync on navigation", error);
246214
});
247215
}
248216
});
249217

250218
chrome.tabs.onRemoved.addListener((tabId) => {
251-
if (state.activeSession && tabId === state.activeSession.tabId) {
219+
const activeSession = getActiveSession();
220+
if (activeSession && tabId === activeSession.tabId) {
252221
endActiveSession("tab_closed")
253222
.then(() => syncCurrentActiveContext("tab_closed"))
254223
.catch((error) => {
@@ -316,9 +285,10 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
316285
}
317286

318287
if (message?.type === "get-runtime-status") {
288+
const runtimeState = sessionEngine.readState();
319289
sendResponse({
320290
ok: true,
321-
activeSession: state.activeSession,
291+
activeSession: runtimeState.activeSession,
322292
paused: state.paused,
323293
retentionDays: state.retentionDays,
324294
idleState: state.idleState

0 commit comments

Comments
 (0)