Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 76 additions & 1 deletion extension/src/background/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import type { AuthMessage, AuthResponse, StoredAuth } from "../types/auth";
import type { ContentMessage } from "../types/content";
import type { SessionMessage, SessionResponse } from "../types/session";
import {
getSession,
startSession,
stopSession,
getElapsedMs,
} from "./session";

const DASHBOARD_URL = import.meta.env.VITE_DASHBOARD_URL as string;
const GOOGLE_CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID as string;
Expand Down Expand Up @@ -101,8 +109,10 @@ export async function apiFetch(

// ─── Message listener ──────────────────────────────────────────────────────────

type IncomingMessage = AuthMessage | SessionMessage | ContentMessage;

chrome.runtime.onMessage.addListener(
(message: AuthMessage, _sender, sendResponse: (r: AuthResponse) => void) => {
(message: IncomingMessage, _sender, sendResponse: (r: AuthResponse | SessionResponse) => void) => {
if (message.type === "AUTH_LOGIN") {
ensureAuthenticated()
.then((jwt) => sendResponse({ success: true, jwt }))
Expand Down Expand Up @@ -144,6 +154,71 @@ chrome.runtime.onMessage.addListener(
return true;
}

// ─── Session messages ────────────────────────────────────────────────────

if (message.type === "SESSION_START") {
startSession()
.then((session) => sendResponse({ success: true, session }))
.catch((err: unknown) =>
sendResponse({
success: false,
error: err instanceof Error ? err.message : "Unknown error",
}),
);
return true;
}

if (message.type === "SESSION_STOP") {
stopSession()
.then((session) => sendResponse({ success: true, session }))
.catch((err: unknown) =>
sendResponse({
success: false,
error: err instanceof Error ? err.message : "Unknown error",
}),
);
return true;
}

if (message.type === "SESSION_GET_STATE") {
getSession()
.then((session) => {
if (session) {
// Attach live elapsed for the popup timer
const sessionWithElapsed = {
...session,
elapsedMs: getElapsedMs(session),
};
sendResponse({ success: true, session: sessionWithElapsed });
} else {
sendResponse({ success: true, session: null });
}
})
.catch((err: unknown) =>
sendResponse({
success: false,
error: err instanceof Error ? err.message : "Unknown error",
}),
);
return true;
}

// ─── Content script messages ─────────────────────────────────────────────

if (message.type === "PAGE_METADATA") {
// Store metadata only when a session is active (AC 5 will batch-send it)
getSession().then((session) => {
if (!session) return;
// Stored for AC 5 batch sync — no response needed
chrome.storage.local.get("pendingEvents").then((r) => {
const pending = (r["pendingEvents"] as unknown[]) ?? [];
pending.push({ ...message.payload, timestamp: new Date().toISOString() });
chrome.storage.local.set({ pendingEvents: pending });
});
});
return false; // no async response needed
}

return false;
},
);
Expand Down
47 changes: 47 additions & 0 deletions extension/src/background/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { Session } from "../types/session";

const STORAGE_KEY = "activeSession";

// ─── Storage ───────────────────────────────────────────────────────────────────

export async function getSession(): Promise<Session | null> {
const r = await chrome.storage.local.get(STORAGE_KEY);
return (r[STORAGE_KEY] as Session) ?? null;
}

async function saveSession(session: Session): Promise<void> {
await chrome.storage.local.set({ [STORAGE_KEY]: session });
}

async function clearSession(): Promise<void> {
await chrome.storage.local.remove(STORAGE_KEY);
}

// ─── Timer ─────────────────────────────────────────────────────────────────────

// Elapsed ms is derived from stored values — safe after SW restart
export function getElapsedMs(session: Session): number {
return session.totalActiveMs + (Date.now() - session.startedAt);
}

// ─── Actions ──────────────────────────────────────────────────────────────────

export async function startSession(): Promise<Session> {
const existing = await getSession();
if (existing) return existing; // idempotent

const session: Session = {
id: crypto.randomUUID(),
startedAt: Date.now(),
totalActiveMs: 0,
};
await saveSession(session);
return session;
}

export async function stopSession(): Promise<Session | null> {
const session = await getSession();
if (!session) return null;
await clearSession();
return session;
}
34 changes: 33 additions & 1 deletion extension/src/content/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,33 @@
console.log("[worktrace] content script loaded", location.href);
import type { ContentMessage, PageMetadata } from "../types/content";

function parseMetadata(): PageMetadata {
const headings = Array.from(document.querySelectorAll("h1, h2, h3"))
.map((el) => el.textContent?.trim() ?? "")
.filter((text) => text.length > 0);

return {
url: window.location.href,
title: document.title,
metaDescription:
document.querySelector<HTMLMetaElement>('meta[name="description"]')
?.content ?? null,
headings,
};
}

function sendMetadata(): void {
const message: ContentMessage = {
type: "PAGE_METADATA",
payload: parseMetadata(),
};
chrome.runtime.sendMessage(message).catch(() => {
// Service worker may be inactive — silently ignore
});
}

// Send on initial load
sendMetadata();

// Re-send on SPA navigation
window.addEventListener("popstate", sendMetadata);
window.addEventListener("hashchange", sendMetadata);
8 changes: 8 additions & 0 deletions extension/src/types/content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface PageMetadata {
url: string;
title: string;
metaDescription: string | null;
headings: string[]; // h1–h3 text content
}

export type ContentMessage = { type: "PAGE_METADATA"; payload: PageMetadata };
14 changes: 14 additions & 0 deletions extension/src/types/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface Session {
id: string;
startedAt: number; // Unix ms
totalActiveMs: number; // accumulated active time (for future pause support)
}

export type SessionMessage =
| { type: "SESSION_START" }
| { type: "SESSION_STOP" }
| { type: "SESSION_GET_STATE" };

export type SessionResponse =
| { success: true; session: Session | null }
| { success: false; error: string };
Loading
Loading