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
5 changes: 5 additions & 0 deletions extension/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,8 @@ VITE_DASHBOARD_URL="http://localhost:3000"
# Application ID: your unpacked extension ID from chrome://extensions
# → copy the generated Client ID here (same value as GOOGLE_CLIENT_ID in dashboard/.env)
VITE_GOOGLE_CLIENT_ID=""

# ─── Dev mode ───────────────────────────────────────────────────────────────────
# true → "Sign in" bypasses Google OAuth and stores a fake JWT (for local UI testing)
# false → real Google OAuth flow (requires valid VITE_GOOGLE_CLIENT_ID + GCC setup)
VITE_DEV_MODE="false"
80 changes: 78 additions & 2 deletions extension/src/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import {
getSession,
startSession,
stopSession,
pauseSession,
resumeSession,
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;
const DEV_MODE = import.meta.env.VITE_DEV_MODE === "true";
const EXPIRY_BUFFER_MS = 60_000;

// ─── Storage ───────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -114,6 +117,17 @@ type IncomingMessage = AuthMessage | SessionMessage | ContentMessage;
chrome.runtime.onMessage.addListener(
(message: IncomingMessage, _sender, sendResponse: (r: AuthResponse | SessionResponse) => void) => {
if (message.type === "AUTH_LOGIN") {
if (DEV_MODE) {
storeAuth("dev.fake.jwt", Date.now() + 7 * 24 * 60 * 60 * 1000)
.then(() => sendResponse({ success: true, jwt: "dev.fake.jwt" }))
.catch((err: unknown) =>
sendResponse({
success: false,
error: err instanceof Error ? err.message : "Unknown error",
}),
);
return true;
}
ensureAuthenticated()
.then((jwt) => sendResponse({ success: true, jwt }))
.catch((err: unknown) =>
Expand Down Expand Up @@ -158,7 +172,12 @@ chrome.runtime.onMessage.addListener(

if (message.type === "SESSION_START") {
startSession()
.then((session) => sendResponse({ success: true, session }))
.then((session) =>
sendResponse({
success: true,
session: { ...session, elapsedMs: getElapsedMs(session) },
}),
)
.catch((err: unknown) =>
sendResponse({
success: false,
Expand All @@ -170,7 +189,12 @@ chrome.runtime.onMessage.addListener(

if (message.type === "SESSION_STOP") {
stopSession()
.then((session) => sendResponse({ success: true, session }))
.then((session) =>
sendResponse({
success: true,
session: session ? { ...session, elapsedMs: getElapsedMs(session) } : null,
}),
)
.catch((err: unknown) =>
sendResponse({
success: false,
Expand Down Expand Up @@ -203,6 +227,58 @@ chrome.runtime.onMessage.addListener(
return true;
}

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

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

if (message.type === "NOTE_ADD") {
getSession().then((session) => {
if (!session || session.pausedAt !== null) return;
chrome.storage.local.get("pendingEvents").then((r) => {
const pending = (r["pendingEvents"] as unknown[]) ?? [];
pending.push({
url: "",
title: "Note",
content: message.text,
tags: ["note", ...message.tags],
timestamp: new Date().toISOString(),
});
chrome.storage.local.set({ pendingEvents: pending });
});
});
return false;
}

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

if (message.type === "PAGE_METADATA") {
Expand Down
28 changes: 28 additions & 0 deletions extension/src/background/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ async function clearSession(): Promise<void> {

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

Expand All @@ -34,6 +35,7 @@ export async function startSession(): Promise<Session> {
id: crypto.randomUUID(),
startedAt: Date.now(),
totalActiveMs: 0,
pausedAt: null,
};
await saveSession(session);
return session;
Expand All @@ -45,3 +47,29 @@ export async function stopSession(): Promise<Session | null> {
await clearSession();
return session;
}

export async function pauseSession(): Promise<Session | null> {
const session = await getSession();
if (!session || session.pausedAt !== null) return session; // idempotent

const updated: Session = {
...session,
totalActiveMs: session.totalActiveMs + (Date.now() - session.startedAt),
pausedAt: Date.now(),
};
await saveSession(updated);
return updated;
}

export async function resumeSession(): Promise<Session | null> {
const session = await getSession();
if (!session || session.pausedAt === null) return session; // idempotent

const updated: Session = {
...session,
startedAt: Date.now(),
pausedAt: null,
};
await saveSession(updated);
return updated;
}
58 changes: 51 additions & 7 deletions extension/src/popup/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,58 @@
<link rel="stylesheet" href="./popup.css" />
</head>
<body>
<main>
<h1>WorkTrace</h1>
<div id="auth-section">
<p id="auth-status">Checking...</p>
<button id="btn-login">Sign in with Google</button>
<button id="btn-logout">Sign out</button>
<div class="popup">

<!-- Header -->
<header class="popup__header">
<div class="popup__logo">
<span class="popup__logo-icon">⬡</span>
<span class="popup__logo-text">WORKTRACE</span>
</div>
<div class="popup__header-right">
<div class="popup__status" id="status-badge">
<span class="popup__status-dot" id="status-dot"></span>
<span class="popup__status-label" id="status-label">IDLE</span>
</div>
<button class="btn btn--logout" id="btn-logout" hidden>⎋</button>
</div>
</header>

<!-- Auth -->
<section class="popup__auth" id="auth-section">
<button class="btn btn--primary" id="btn-login">Sign in with Google</button>
</section>

<!-- Timer -->
<section class="popup__timer" id="timer-section">
<div class="popup__time" id="timer">00:00:00</div>
<div class="popup__controls">
<button class="btn btn--start" id="btn-start">▶ START</button>
<button class="btn btn--pause" id="btn-pause" disabled>⏸ PAUSE</button>
<button class="btn btn--stop" id="btn-stop" disabled>■ STOP</button>
</div>
</section>

<!-- Sync -->
<div class="popup__sync" id="sync-indicator">
<span class="popup__sync-icon">⚡</span>
<span class="popup__sync-count" id="sync-label">0 events queued</span>
</div>
</main>

<!-- Notes -->
<section class="popup__notes">
<div class="popup__field">
<label class="popup__field-label">NOTE</label>
<input class="popup__field-input" id="note-input" type="text" placeholder="what are you working on..." />
</div>
<div class="popup__field">
<label class="popup__field-label">TAGS</label>
<input class="popup__field-input" id="tags-input" type="text" placeholder="react, bug, refactor..." />
</div>
<button class="btn btn--note" id="btn-note">+ LOG NOTE</button>
</section>

</div>
<script type="module" src="./popup.ts"></script>
</body>
</html>
Loading
Loading