# Project export: FocusForge

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: CruzHacks 2026
- Tagline: FocusForge turns messy browser tabs into clear workspaces, next actions, and a “resume where you left off” summary. So, you can switch between tasks without losing momentum long-term.
- Devpost: https://devpost.com/software/focusforge-hydjiu
- GitHub: https://github.com/isschrack/FocusForge.git
- Demo: https://focusforge-app-seven.vercel.app/
- Video: https://www.youtube.com/embed/Ah4erUsQGw8?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 4 GitHub contributor(s) — Izzy Schrack (56 commits), sikaatwa (24 commits), Sakshi Tripathy (23 commits), Sakshi Tripathy (19 commits)

## Devpost submission (written by the team)

### Inspiration

We built FocusForge because we kept running into the same problem: real work on Chrome turns into dozens of tabs across docs, videos, code, tickets, and references. After a break—or even the next day—you don’t just forget which tab to open. You forget the context: what mattered, where you left off, and what to do next. Reconstructing that mental state wastes time and kills momentum. For a student, it’s the classic spiral: you’re researching for an assignment, watching one “helpful” video, cross-checking notes, opening a reference paper, and suddenly your browser is a wall of tabs. You step away for dinner, come back, and spend the first 10 minutes just trying to remember what you were even doing—while the deadline is still the same. For a working professional, it’s even worse. Work is never one thing at a time: Slack pings, meetings, tickets, dashboards, internal docs, PR reviews, and random “quick checks” that hijack your flow. You might be productive for an hour, but later you can’t answer the simplest question: What did I actually make progress on, and what’s the next step? And for everyone—students, employees, creators—Chrome becomes a messy memory dump. You keep tabs open because you’re afraid you’ll lose something important. You close them and regret it later. Either way, your brain ends up doing the work a tool should do: rebuilding context from scratch every time you restart. We wanted a way to “pause life” and then resume instantly—so instead of re-orienting yourself manually, you can come back and immediately see what mattered, what you touched, and what to do next.

### What it does

FocusForge helps you resume faster when you’ve lost track. You start a focus session from our Chrome extension, and FocusForge tracks your browsing activity in a privacy-safe way—only URL, page title, and timestamps (no page content, no keystrokes). When you stop the session, FocusForge generates a Session Resume that brings your context back in seconds: Last Stop: the exact page you ended on, so you can jump back immediately Workspaces: where you actually spent time (top sites/domains), so you know what mattered Timeline: a clean trace of what you touched and when, so the session is reconstructable AI Resume + Next Steps: a grounded recap and actionable steps to continue without rethinking You can create multiple sessions so each workflow—coding, job search, studying, creative work, admin tasks—stays separated and easy to pick back up later. To make the session truly persistent, you can also export the full resume to an Opennote Journal, turning each session into a searchable, organized note you can revisit anytime.

### How we built it

e designed FocusForge as a simple loop: capture → store → summarize → resume → export. Chrome Extension (tracking layer) Starts/stops focus sessions and logs tab activity in real time (URL, title, timestamps). This is how we capture “what happened” without reading page content. Backend API (session controller) Receives session + event logs, handles session lifecycle (start/stop/idle), and exposes endpoints to fetch recent sessions and session details for the UI. Supabase (Postgres database) Stores session metadata and the event timeline (sessions + session_events + analysis output). This makes sessions persistent, searchable, and demo-stable. AI Summary (Gemini) Converts the session trace into a grounded “Session Resume” and next-step suggestions—based on captured URLs/titles, not page content. Web App UI (Figma → implementation) A dashboard for recent sessions and a Resume Panel that surfaces the most important context quickly (last stop, workspaces, timeline, recap). Opennote API Integration Exports the final Session Resume into an Opennote Journal so the recap becomes a clean, persistent note you can revisit and build on.

### Challenges we ran into

Integrating Opennote cleanly and formatting exports so they look polished and useful. Integrating Opennote cleanly and formatting exports so they look polished and useful. Keeping scope focused: avoiding feature bloat and staying centered on “resume where you left off.” Keeping scope focused: avoiding feature bloat and staying centered on “resume where you left off.” Demo stability: making sure sessions/events/summaries don’t break with edge cases under time pressure. Demo stability: making sure sessions/events/summaries don’t break with edge cases under time pressure. Defining “focus” without being invasive: We wanted meaningful insights while staying privacy-safe (no page content/keystrokes), so we had to make summaries and intent alignment work using only URLs/titles/timestamps. Defining “focus” without being invasive: We wanted meaningful insights while staying privacy-safe (no page content/keystrokes), so we had to make summaries and intent alignment work using only URLs/titles/timestamps. Reliable session boundaries: Handling real-life behavior like idle time, laptop sleep, and accidental “leave session running” cases without corrupting the timeline or breaking demo flow. Reliable session boundaries: Handling real-life behavior like idle time, laptop sleep, and accidental “leave session running” cases without corrupting the timeline or breaking demo flow. Consistency across components: Keeping the extension, backend, database, and web UI in sync so session IDs, events, and summaries always match—even when users refresh or restart. Consistency across components: Keeping the extension, backend, database, and web UI in sync so session IDs, events, and summaries always match—even when users refresh or restart. Making AI outputs trustworthy: Preventing “hallucinated” summaries and ensuring the recap stays grounded in what actually happened during the session. Making AI outputs trustworthy: Preventing “hallucinated” summaries and ensuring the recap stays grounded in what actually happened during the session. Turning raw events into useful structure: Converting messy tab-switch logs into “workspaces,” time breakdowns, and a readable timeline that feels instantly understandable. Turning raw events into useful structure: Converting messy tab-switch logs into “workspaces,” time breakdowns, and a readable timeline that feels instantly understandable. Last-minute polish without breaking core: Improving UI/UX while avoiding risky refactors that could destabilize tracking or exports right before demo. Last-minute polish without breaking core: Improving UI/UX while avoiding risky refactors that could destabilize tracking or exports right before demo.

### Accomplishments we're proud of

Izzy: I'm proud of the hover effects I added to the buttons to make using the interface more fun. I also added the graphs to the Resume Panel which I think helps the user look over their stats more. Nikitha: I'm proud of building apps in html/css/js to working with APIs and implementing a supabase backend to persist session, event, and analysis data for a chrome extension MVP. Sakshi - Im proud of building the project skeleton + backend pipeline (sessions/events → Supabase) so the core loop is stable and demo-ready. I Implemented session lifecycle logic (start/stop + inactivity handling) to match real user behavior without corrupting timelines. I also Integrated Opennote export, turning each Session Resume into a clean, structured journal entry you can revisit later and kept the system reliable under demo pressure by prioritizing stability and edge-case handling over risky last-minute features.

### What we learned

Nikitha: As a first-time hacker, I learned a lot of things. I learned how to build and ship a product under time constraints. I learned how to turn a prototype into a stable, demo-ready backend and gained experience in designing a reliable database schema. I also learned how to use a modern backend platform instead of building everything from scratch by using supabase as a managed backend and connecting frontend to backend through APIs. Sakshi - I learned how to design an event-based session model (sessions + session_events + analysis) so raw tab pings can later be reconstructed into a timeline/workspaces without losing integrity. I also learned that productivity apps live or die on session boundaries—handling inactivity, laptop sleep, and accidental long-running sessions needs clear rules + consistent behavior across all endpoints. I learned how to build integration-safe exports: converting internal JSON into a clean, structured Opennote journal template that stays readable and doesn’t break when fields are missing.

### What's next

Better AI summaries UX improvements Build a personalized intent model: App stores and learns the user's definition of focus and their repeated workflows Add automatic task extraction and execution tracking: Detects todos, schedules them and check for completion in the next session Cross-device continuity: You stop on laptop, resume on phone with the same context and "next step". Personalized intent model: learn each user’s definition of “focus” over time (their common workflows + what they consider distraction) and adapt alignment scoring accordingly. Smarter “Resume Session”: restore a full workspace (top tabs/domains) instead of only the last stop, so users can truly continue in one click. Action tracking across sessions: extract next actions, let users mark them done, and show “carried-over tasks” when they resume later. Better Opennote integration: auto-journal on session end + daily/weekly rollups inside Opennote (sessions grouped, searchable, and reviewable).

## README (from the GitHub repository)

# FocusForge

Resume work without rethinking.

FocusForge preserves intent and decision context across browser sessions so you
can resume meaningful work instantly. It is not a task manager, not a note app,
and not a productivity tracker. It is intent memory.

## Problem

When people return to work, their files are still open but their thinking is
gone. Re-orienting costs 10 to 20 minutes per session. Existing tools preserve
content, not decision context.

## What FocusForge Is / Is Not

FocusForge is:
- A session-based intent memory
- A system that extracts next actions and decisions
- A way to resume work without cognitive reload

FocusForge is not:
- A task manager
- A note app
- A productivity tracker or habit app
- A ChatGPT wrapper

## Core User Flow

1) Start a focus session (enter a goal).
2) Auto-capture lightweight context (active tab URL, title, timestamps).
3) End session and run analysis (next actions, decisions, resume summary).
4) Resume session on one clean screen (goal, actions, decisions, grouped links).

## Key Differentiators

- Intent memory: "You were drafting a resume bullet for backend roles."
- Decision awareness: "Need to decide whether to apply via referral."
- Cognitive compression: one goal, 3 to 5 actions, grouped links.

## Privacy Stance

- Only active tab URL + title + timestamps.
- No page content, no keystrokes.
- Pause/Resume anytime.

## Architecture (High Level)

Chrome Extension (MV3) -> Next.js API -> Supabase -> Analysis -> UI

The extension sends tab activation events. The app computes durations, groups
workspaces, and calls an LLM to generate next actions, pending decisions, and a
resume summary.

## Data Model

Session
```
{
  "id": "uuid",
  "goal": "Apply to SWE roles",
  "started_at": "...",
  "ended_at": "...",
  "raw_context": "...",
  "resume_summary": "...",
  "next_actions": [],
  "pending_decisions": [],
  "link_groups": []
}
```

Event payload (from extension)
```
{
  "sessionId": "uuid",
  "ts": 1730000000000,
  "type": "TAB_ACTIVE",
  "url": "https://leetcode.com/problems/two-sum",
  "title": "Two Sum - LeetCode"
}
```

## API Endpoints

- POST `/api/session/start` -> `{ sessionId }`
- POST `/api/event` -> stores activity events
- POST `/api/session/pause` / `/api/session/resume`
- POST `/api/session/end` -> triggers analysis
- GET `/api/session/:id` -> session + analysis + timeline
- POST `/api/opennote/journal/export` -> optional journal export

## Analysis Pipeline

1) Deterministic summary:
   - Sort events, compute duration per tab
   - Extract domains
   - Aggregate workspaces and top URLs
   - Identify last stop
2) LLM enrichment:
   - Resume summary (1 to 2 sentences)
   - Next actions (3 to 5, verb-leading)
   - Pending decisions (1 to 3)

## UI Screens

- `/` Dashboard (recent sessions, open session, demo)
- `/session/[id]` Resume screen (main demo view)
- `/session/demo` Demo fallback
- `/session/live` Live session view

## Repo Structure

- `app/` Next.js App Router UI + API routes
- `lib/` Analysis, grouping, and data helpers
- `extension/` Chrome extension (MV3) that sends events
- `backend/` Legacy Pages Router API (kept for reference)
- `public/` Static assets

## Getting Started

### Prereqs

- Node 18+
- Supabase project (or local fallback)
- Optional: Opennote API key for journal export

### Install

```
npm install
```

### Environment

Create `.env.local` in the project root:
```
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
SUPABASE_SERVICE_ROLE_KEY=...
OPENNOTE_API_KEY=... (optional)
```

See `ENV_SETUP.md` for details.

### Run

```
npm run dev
```

Open `http://localhost:3000`.

## Chrome Extension (MV3)

1) Open Chrome -> Extensions -> Enable Developer mode.
2) Load unpacked -> select `extension/`.
3) In the popup, set intent and start session.
4) Browse normally; events are posted to the web app.

The extension defaults to `http://localhost:3000` and auto-detects Vercel
domains when available.

## Deployment

- Deploy with Vercel.
- Ensure env vars are set in the Vercel project.

## Demo Tips

- Preload 2 to 3 sessions (job apps, interview prep, hackathon planning).
- Show messy browsing, end session, then open resume screen instantly.
- Use the "resume summary" line: "Last time, you were working on X..."

## Non-Negotiables

- Keep scope tight
- Do not add features outside intent memory
- UI must feel finished
- Demo must be instant

## Detected evidence (automated analysis)

Indexed codebase: 93 recognized source files, 463 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Next.js (technology) — detected in the code
- Python (language) — detected in the code
- React (technology) — detected in the code
- SQL (language) — detected in the code
- Supabase (technology) — detected in the code
- Tailwind CSS (technology) — detected in the code
- TypeScript (language) — detected in the code
- Google Gemini (technology) — claimed on Devpost, not found in the code
- Vercel (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (105 of 105)

```
.gitignore
AGENT_ANALYSIS.md
AGENT_ARCHITECTURE.md
agent_planner.py
AI_ANALYSIS_SUMMARY.md
analyzer.py
app/api/cors.ts
app/api/event/route.ts
app/api/opennote/journal/export/route.ts
app/api/plan/route.ts
app/api/session/[id]/plan/route.ts
app/api/session/[id]/route.ts
app/api/session/end/route.ts
app/api/session/intent/route.ts
app/api/session/pause/route.ts
app/api/session/resume/route.ts
app/api/session/start/route.ts
app/api/sessions/route.ts
app/globals.css
app/layout.tsx
app/page.tsx
app/RecentSessions.tsx
app/session/_components/SessionDetail.tsx
app/session/[id]/page.tsx
app/session/demo/page.tsx
app/session/live/page.tsx
app/session/live/SessionLiveClient.tsx
backend/.gitignore
backend/analyzer_server_requirements.txt
backend/analyzer_server.py
backend/CHANGES.md
backend/DEPLOYMENT.md
backend/IMPLEMENTATION.md
backend/INTEGRATION-STATUS.md
backend/INTEGRATION-VERIFICATION.md
backend/lib/auto-pause.ts
backend/lib/db.types.ts
backend/lib/idle-policy.ts
backend/lib/opennote.ts
backend/lib/supabase.ts
backend/lib/utils.ts
backend/next.config.js
backend/package.json
backend/pages/api/analyze.ts
backend/pages/api/check-idle.ts
backend/pages/api/event.ts
backend/pages/api/opennote/journal/export.ts
backend/pages/api/plan.ts
backend/pages/api/session/[id].ts
backend/pages/api/session/[id]/eventsWithDuration.ts
backend/pages/api/session/[id]/plan.ts
backend/pages/api/session/end.ts
backend/pages/api/session/pause.ts
backend/pages/api/session/resume.ts
backend/pages/api/session/start.ts
backend/pages/api/sessions.ts
backend/QUICKSTART.md
backend/README.md
backend/scripts/analyze.py
backend/supabase-schema.sql
backend/test-integration.md
backend/test-opennote-journal-real.ts
backend/tsconfig.json
backend/vercel.json
backend/verify-integration.py
ENV_SETUP.md
eslint.config.mjs
extension/background.js
extension/content.js
extension/manifest.json
extension/popup.html
extension/popup.js
gemini_analyzer_with_tools.py
gemini_analyzer.py
INTENT_ALIGNMENT_AGENT.md
JOURNAL_EXPORT_DEBUG.md
lib/analysis.ts
lib/classify.ts
lib/grouping.ts
lib/intent_analyzer.ts
lib/opennote.ts
lib/store.ts
lib/supabase.ts
lib/types.ts
lib/utils.ts
middleware.ts
next.config.ts
package.json
PLANNER_AGENT_SUMMARY.md
postcss.config.mjs
README.md
requirements.txt
supabase-schema.sql
test_analyzer.py
test_planner_dummy.py
test_planner.py
test_planning_agent.ts
test_tools.py
TOOLS_IMPLEMENTATION.md
TOOLS_SETUP.md
tools/__init__.py
tools/calendar_tool.py
tools/email_tool.py
tools/tool_registry.py
tsconfig.json
```

### Dependencies

- backend/package.json: @supabase/supabase-js@^2.38.0, @types/node@^20.0.0, @types/react@^18.2.0, @types/react-dom@^18.2.0, next@^14.0.0, react@^18.2.0, react-dom@^18.2.0, typescript@^5.0.0
- package.json: @supabase/supabase-js@^2.90.1, @tailwindcss/postcss@^4, @types/node@^20, @types/react@^19, @types/react-dom@^19, eslint@^9, eslint-config-next@16.1.3, geist@^1.5.1, next@16.1.3, react@19.2.3, react-dom@19.2.3, tailwindcss@^4, typescript@^5
- requirements.txt: google-api-python-client@>=2.0.0, google-auth-httplib2@>=0.1.0, google-auth-oauthlib@>=0.5.0, google-generativeai@>=0.3.0, pyparsing@>=3.0.4,<4

### Recent commits (newest first)

- Harden
- Align
- Add
- Last fixes for journal API.
- Enhance
- Merge origin/main into Sakshi
- Refine alignment, tasks, and evidence-based categories
- Remove hardcoded 'Not enough data' message and add hover tooltips to Intent Alignment bar: show percentages on hover like pie chart
- Fix Intent Alignment stacked bar graph: use absolute positioning to properly display all segments including small aligned portions
- Merge remote-tracking branch 'origin/main' into Izzy-Final-Touches
- Add task descriptions and remove Next actions section: generate actionable descriptions for tasks, update resume/continue tasks to mention button actions, remove Next actions section from UI
- Merge pull request #7 from isschrack/testing
- Fix Supabase URL validation to prevent build errors with placeholder values
- Merge main into testing - preserving main's functionality
- Improve Supabase configuration and error handling for Opennote journal export
- Fix Time Breakdown pie chart rendering: filter zero-time segments, fix full circle path calculation, add explicit SVG dimensions
- Update heading text: 'Review top 3 pages visited' to 'Top Pages Visited'
- Improve task UI: enhance priority tags, make Session ID clickable, remove Strategic Suggestions and urgency tags
- Merge remote-tracking branch 'origin/main' into Sakshi
- Merge remote-tracking branch 'origin/main' into Izzy-Final-Touches

## Key source files (fetched from GitHub, selected and truncated for size)

### ENV_SETUP.md

```markdown
# Environment Variables Setup

## Required for Journal Export

The journal export feature requires Supabase environment variables to be set.

### Required Variables

Create a `.env.local` file in the **root directory** (same level as `package.json`) with:

```bash
NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key
OPENNOTE_API_KEY=your_opennote_api_key
```

### Where to Get Supabase Credentials

1. Go to your Supabase project: https://supabase.com/dashboard
2. Select your project
3. Go to **Settings** → **API**
4. Copy:
   - **Project URL** → `NEXT_PUBLIC_SUPABASE_URL`
   - **anon public** key → `NEXT_PUBLIC_SUPABASE_ANON_KEY`
   - **service_role** key → `SUPABASE_SERVICE_ROLE_KEY` (⚠️ Keep this secret!)

### File Location

**Important**: The `.env.local` file should be in the **root directory** of the project:
```
FocusForge-1/
├── .env.local          ← HERE (root level)
├── package.json
├── app/
├── backend/
└── lib/
```

### After Setting Variables

1. **Restart your dev server** (if running):
   ```bash
   # Stop the server (Ctrl+C)
   npm run dev
   ```

2. **Verify variables are loaded**:
   The error message will now tell you which variables are missing.

### Quick Check

Run this to verify your setup:
```bash
# Check if variables are set (without exposing values)
node -e "console.log('SUPABASE_URL:', process.env.NEXT_PUBLIC_SUPABASE_URL ? 'SET' : 'MISSING'); console.log('SUPABASE_KEY:', process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ? 'SET' : 'MISSING');"
```

### Troubleshooting

**Error: "Supabase not configured"**
- Check `.env.local` exists in root directory
- Verify variable names are correct (case-sensitive)
- Restart dev server after adding variables
- Check `.env.local` is not in `.gitignore` (it should be ignored, but file should exist locally)

**Error: "Session not found"**
- Supabase is configured, but session doesn't exist in database
- Check session ID is correct
- Verify database tables are created (run `backend/supabase-schema.sql`)

**Error: "OPENNOTE_API_KEY environment variable is not set"**
- Add `OPENNOTE_API_KEY=sk_opennote_...` to `.env.local`
- Restart dev server

```

### JOURNAL_EXPORT_DEBUG.md

```markdown
# Debugging Journal Export Internal Server Error

## Common Causes

### 1. Missing OPENNOTE_API_KEY
**Error**: "OPENNOTE_API_KEY environment variable is not set"

**Solution**: 
- Set `OPENNOTE_API_KEY` in your environment variables (`.env.local` or Vercel dashboard)
- Restart the server after adding

### 2. Backend Route Not Accessible
**Error**: "Failed to reach backend" or connection refused

**Causes**:
- App Router route can't reach Pages Router route on same host
- URL construction is wrong
- Backend server not running

**Solution**:
- Check server logs for fetch URL being used
- Ensure `BACKEND_API_URL` is set if backend is separate
- In same deployment, ensure URL is `http://localhost:3000/api/opennote/journal/export` (dev) or `https://yourdomain.com/api/opennote/journal/export` (prod)

### 3. Supabase Connection Issues
**Error**: "Session not found" or database errors

**Solution**:
- Check `SUPABASE_SERVICE_ROLE_KEY` is set
- Verify session ID exists in database
- Check Supabase project is active

### 4. Opennote API Errors
**Error**: "Opennote API error: 401" or similar

**Solution**:
- Verify `OPENNOTE_API_KEY` is valid
- Check Opennote API URL is correct (default: `https://api.opennote.com`)
- Ensure API key has journal export permissions

## How to Debug

### 1. Check Server Logs
When you click "Export to Opennote Journal", check your server console for:
- `[Journal Export] Using fetch URL: ...` - Shows which URL is being called
- `Opennote journal export - fetch error: ...` - Shows fetch failures
- `Opennote export error: ...` - Shows Opennote API errors

### 2. Check Browser Console
Look for:
- Network request to `/api/opennote/journal/export`
- Response status and error message
- Any CORS errors

### 3. Test Backend Route Directly
```bash
curl -X POST http://localhost:3000/api/opennote/journal/export \
  -H "Content-Type: application/json" \
  -d '{"sessionId":"YOUR_SESSION_ID"}'
```

### 4. Verify Environment Variables
```bash
# In backend directory
echo $OPENNOTE_API_KEY
echo $SUPABASE_SERVICE_ROLE_KEY
```

## Quick Fixes

### If fetch URL is wrong:
The App Router route constructs the URL automatically. If it's wrong:
1. Set `BACKEND_API_URL=http://localhost:3000` (for local dev)
2. Or `BACKEND_API_URL=https://your-backend.vercel.app` (for prod)

### If missing API key:
```bash
# In .env.local (backend directory or root)
OPENNOTE_API_KEY=sk_opennote_...
```

### If session not found:
- Ensure session exists in Supabase database
- Check session ID is correct
- Verify Supabase credentials are set

## Expected Flow

1. **Frontend** → Calls `/api/opennote/journal/export` (App Router)
2. **App Router** → Proxies to `/api/opennote/journal/export` (Pages Router)
3. **Backend Route** → Fetches from Supabase:
   - Session data
   - Events
   - Analysis
4. **Backend Route** → Generates markdown
5. **Backend Route** → Calls Opennote API
6. **Ba
[truncated — 635 more characters]
```

### requirements.txt

```
# FocusForge dependencies

# For Gemini API (optional, for intelligent analysis)
# Note: google.generativeai is deprecated but still works. Future versions should use google.genai
google-generativeai>=0.3.0

# Dependencies required by google-generativeai
# httplib2 requires pyparsing>=3.0.4
pyparsing>=3.0.4,<4

# For tool use (calendar and email) - optional
google-api-python-client>=2.0.0
google-auth-httplib2>=0.1.0
google-auth-oauthlib>=0.5.0
```

### package.json

```
{
  "name": "focusforge-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint"
  },
  "dependencies": {
    "@supabase/supabase-js": "^2.90.1",
    "geist": "^1.5.1",
    "next": "16.1.3",
    "react": "19.2.3",
    "react-dom": "19.2.3"
  },
  "devDependencies": {
    "@tailwindcss/postcss": "^4",
    "@types/node": "^20",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "eslint": "^9",
    "eslint-config-next": "16.1.3",
    "tailwindcss": "^4",
    "typescript": "^5"
  }
}

```

### backend/package.json

```
{
  "name": "focusforge-backend",
  "version": "1.0.0",
  "description": "FocusForge backend API",
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "next": "^14.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "@supabase/supabase-js": "^2.38.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0",
    "typescript": "^5.0.0"
  }
}

```

### app/layout.tsx

```typescript
import type { Metadata } from "next";
import { Jura, Lato } from "next/font/google";
import "./globals.css";

// Font loaders require all values to be literals, not dynamic config
// Theme config in next.config.ts is for reference/documentation and colors only
const jura = Jura({
  variable: "--font-jura",
  subsets: ["latin"],
  display: "swap",
});

const lato = Lato({
  variable: "--font-lato",
  subsets: ["latin"],
  weight: ["400", "700"],
  display: "swap",
});

export const metadata: Metadata = {
  title: "FocusForge",
  description: "Track browser focus sessions and resume with context.",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body
        className={`${jura.variable} ${lato.variable} antialiased`}
      >
        {children}
      </body>
    </html>
  );
}

```

### app/page.tsx

```typescript
import Image from "next/image";
import Link from "next/link";

import { RecentSessions } from "@/app/RecentSessions";

export default function Home() {
  return (
    <div
      className="min-h-screen px-6 py-12 text-zinc-900"
      style={{
        backgroundColor: "#BDE8F5",
        backgroundImage: `
          radial-gradient(circle, rgba(50, 87, 142, 0.2) 1.5px, transparent 1.5px),
          repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(255, 255, 255, 0.03) 2px, rgba(255, 255, 255, 0.03) 4px),
          repeating-linear-gradient(90deg, transparent, transparent 2px, rgba(0, 0, 0, 0.02) 2px, rgba(0, 0, 0, 0.02) 4px)
        `,
        backgroundSize: "28px 28px, 3px 3px, 3px 3px",
      }}
    >
      <div className="mx-auto flex w-full max-w-5xl flex-col gap-10">
        <div className="rounded-2xl border border-zinc-200 bg-white p-8 shadow-sm">
          <header>
            <div className="flex items-center justify-center gap-3">
              <Image
                src="/focus_forge_logo.png"
                alt="FocusForge"
                width={90}
                height={90}
                className="h-[90px] w-[90px]"
              />
              <h1
                className="text-[90px] font-semibold leading-none"
                style={{
                  fontFamily: "var(--font-jura), sans-serif",
                  color: "#32578E",
                }}
              >
                FocusForge
              </h1>
            </div>
            <h2
              className="mt-4 text-center text-3xl font-semibold"
              style={{
                fontFamily: "var(--font-jura), sans-serif",
                color: "#669EE6",
              }}
            >
              Auto-track browser sessions and resume faster.
            </h2>
            <p className="mx-auto mt-4 max-w-2xl text-center text-sm" style={{ color: "#8f8f9f" }}>
              FocusForge only captures the active tab&apos;s URL, title, and
              timestamps. No page content, no keystrokes, and no personal data
              beyond what your browser already exposes.
            </p>
            <div className="mt-6 flex flex-wrap justify-center gap-3">
              <Link
                href="/session/live"
                className="cursor-pointer rounded-lg px-4 py-2 text-sm font-medium text-white shadow-sm transition-all duration-200 hover:scale-105 hover:shadow-lg hover:opacity-90"
                style={{
                  fontFamily: "var(--font-jura), sans-serif",
                  backgroundColor: "#32578E",
                  borderColor: "#32578E",
                }}
              >
                Open Session
              </Link>
              <Link
                href="/session/demo"
                className="cursor-pointer rounded-lg border px-4 py-2 text-sm font-medium transition-all duration-200 hover:bg-zinc-100 hover:scale-105 hover:shadow-md"
                style={{
                  fontFamily: "var(--font-jura), sans-serif",
                  color: "#32578E",
                  borderColor: "#32578E",
                }}
              >
                View Demo
              </Link>
            </div>
          </header>

          <section className="mt-10 border-t border-zinc-200 pt-8">
            <h2
              className="text-lg font-semibold"
              style={{
                fontFamily: "var(--font-jura), sans-serif",
                color: "#669EE6",
              }}
            >
              Recent Sessions
            </h2>
            <RecentSessions />
          </section>
        </div>
      </div>
    </div>
  );
}

```

### app/api/sessions/route.ts

```typescript
import { corsHeaders, corsJson } from "@/app/api/cors";
import { computeSummary } from "@/lib/grouping";
import { getEvents, listSessions } from "@/lib/store";

export async function OPTIONS() {
  return new Response(null, { status: 204, headers: corsHeaders });
}

type SessionListItem = {
  id: string;
  status: string;
  started_at: number;
  ended_at?: number;
  durationSec: number;
  topWorkspaces: string[];
};

export async function GET() {
  const sessions = (await listSessions()).slice(0, 10);
  const payload: SessionListItem[] = await Promise.all(
    sessions.map(async (session) => {
      const events = await getEvents(session.id);
      const summary = computeSummary(session, events);
      const timelineDurationSec = summary.timeline.reduce((total, event) => {
        if (event.type !== "TAB_ACTIVE") {
          return total;
        }
        return total + (event.durationSec ?? 0);
      }, 0);
      const fallbackDurationSec =
        session.ended_at && session.started_at
          ? Math.max(0, Math.round((session.ended_at - session.started_at) / 1000))
          : 0;
      const durationSec =
        timelineDurationSec > 0 ? timelineDurationSec : fallbackDurationSec;
      const topWorkspaces = summary.domains
        .slice(0, 2)
        .map((domain) => domain.label);

      return {
        id: session.id,
        status: session.status,
        started_at: session.started_at,
        ended_at: session.ended_at,
        durationSec,
        topWorkspaces,
      };
    }),
  );

  return corsJson({ sessions: payload });
}

```

### app/session/demo/page.tsx

```typescript
import { SessionDetail } from "@/app/session/_components/SessionDetail";
import { computeSummary } from "@/lib/grouping";
import { Event, Session } from "@/lib/types";

export default function DemoSessionPage() {
  const now = Date.now();
  const session: Session = {
    id: "demo-session",
    started_at: now - 45 * 60 * 1000,
    ended_at: now - 5 * 60 * 1000,
    status: "ended",
    intent_raw: "interview prep",
    intent_tags: ["interview prep"],
  };

  const events: Event[] = [
    {
      sessionId: session.id,
      ts: now - 44 * 60 * 1000,
      type: "TAB_ACTIVE",
      url: "https://docs.google.com/document/d/123",
      title: "FocusForge planning doc",
    },
    {
      sessionId: session.id,
      ts: now - 38 * 60 * 1000,
      type: "TAB_ACTIVE",
      url: "https://github.com/isschrack/FocusForge",
      title: "FocusForge repo",
    },
    {
      sessionId: session.id,
      ts: now - 30 * 60 * 1000,
      type: "PAUSE",
      url: "",
      title: "",
    },
    {
      sessionId: session.id,
      ts: now - 26 * 60 * 1000,
      type: "RESUME",
      url: "",
      title: "",
    },
    {
      sessionId: session.id,
      ts: now - 22 * 60 * 1000,
      type: "TAB_ACTIVE",
      url: "https://leetcode.com/problems/two-sum/",
      title: "Two Sum - LeetCode",
    },
    {
      sessionId: session.id,
      ts: now - 12 * 60 * 1000,
      type: "TAB_ACTIVE",
      url: "https://calendar.google.com",
      title: "Calendar",
    },
    {
      sessionId: session.id,
      ts: now - 5 * 60 * 1000,
      type: "STOP",
      url: "",
      title: "",
    },
  ];

  const computedSummary = computeSummary(session, events);

  return (
    <SessionDetail session={session} computedSummary={computedSummary} />
  );
}

```

### app/api/event/route.ts

```typescript
import { corsHeaders, corsJson } from "@/app/api/cors";
import { addEvent, getEvents, getSession, updateSessionStatus } from "@/lib/store";
import { Event } from "@/lib/types";

const IDLE_THRESHOLD_MS = 30 * 60 * 1000;
const HARD_BREAK_THRESHOLD_MS = 2 * 60 * 60 * 1000;

export async function OPTIONS() {
  return new Response(null, { status: 204, headers: corsHeaders });
}

export async function POST(request: Request) {
  const body = (await request.json()) as Partial<Event>;

  if (!body.sessionId || !body.type || !body.ts) {
    return corsJson(
      { error: "sessionId, type, and ts are required." },
      { status: 400 },
    );
  }

  const event: Event = {
    sessionId: body.sessionId,
    ts: body.ts,
    type: body.type,
    url: body.url ?? "",
    title: body.title ?? "",
  };

  const session = await getSession(body.sessionId);
  const events = await getEvents(body.sessionId);
  const lastEvent = events.length > 0 ? events[events.length - 1] : undefined;
  const lastRealEvent =
    lastEvent?.type === "BREAK" && events.length > 1
      ? events[events.length - 2]
      : lastEvent;
  if (lastRealEvent) {
    const gapMs = event.ts - lastRealEvent.ts;
    if (gapMs > HARD_BREAK_THRESHOLD_MS && session?.status === "running") {
      await updateSessionStatus(body.sessionId, "auto_ended", lastRealEvent.ts);
      await addEvent({
        sessionId: body.sessionId,
        ts: lastRealEvent.ts,
        type: "STOP",
        url: "",
        title: "Auto-ended due to inactivity",
      });
      return corsJson(
        { error: "Session auto-ended due to inactivity.", autoEnded: true },
        { status: 409 },
      );
    }
    if (gapMs > IDLE_THRESHOLD_MS) {
      await addEvent({
        sessionId: body.sessionId,
        ts: lastRealEvent.ts,
        type: "BREAK",
        url: "",
        title: "Break",
      });
    }
  }

  await addEvent(event);
  return corsJson({ ok: true });
}

```

[83 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]