Skip to content

feat: initial full project - Chrome extension + Next.js dashboard (Weeks 1–4)#25

Merged
BODMAT merged 24 commits into
mainfrom
development
Jun 2, 2026
Merged

feat: initial full project - Chrome extension + Next.js dashboard (Weeks 1–4)#25
BODMAT merged 24 commits into
mainfrom
development

Conversation

@BODMAT

@BODMAT BODMAT commented Jun 2, 2026

Copy link
Copy Markdown
Owner

First sync of all development work into main. Full WorkTrace product - 24 commits, 173 files.

WorkTrace is a Chrome extension + Next.js dashboard that captures developer session context (visited pages, notes, tags, music) and generates AI-powered productivity reports via Groq LLM.

Extension

  • Google OAuth with JWT, session timer (pause/resume), notes and tags
  • Content script captures page metadata per configurable parsing mode (full/headings/meta)
  • Batched event sync with 30s alarm and retry queue
  • YouTube Music and SoundCloud track capture with listening time
  • Per-domain privacy blocklist
  • CI/CD: zip artifact auto-released on tag via GitHub Actions

Dashboard

  • Google Sign-In with session cookie and middleware route guard
  • Event feed with cursor pagination, date/tag filters, activity charts
  • Top sessions ranked by SQL window functions
  • AI session report via Groq LLM with token-budget context sampling and per-user encrypted API key (AES-256-GCM)
  • Music analytics with productivity correlation
  • Framer Motion animations, scroll-reactive parallax background
  • Delete account with cascade cleanup
  • Deployed to Vercel

Closes

  • Weeks 1-4 all ACs

BODMAT added 24 commits May 13, 2026 11:53
Scaffold Next.js 16 app with App Router and Tailwind v4.
Add Prisma, Zod, TanStack Query v5, Framer Motion, D3,
google-auth-library, jsonwebtoken. Document env vars in
.env.example.

Closes Week 1 AC 1.
Add docker-compose.yml with Postgres 16 on port 5433 (avoids
conflict with host-installed Postgres on 5432). Add User,
Session, Event, Track models with cascade deletes and FK indexes.
Run first migration (init), applied locally and to Neon.

Also fix .env.example exclusion in dashboard/.gitignore that
silently dropped the file from the AC 1 PR.

Closes Week 1 AC 2.
Set up the AI rulebook before introducing AI-generated code in AC 7:
- CLAUDE.md with type-safety, validation, dashboard structure,
  auth, extension isolation, and Prisma rules
- .claude/skills/{api-route,prisma-model,extension-message}.md
  with task-specific recipes and code examples
- dashboard/server/ folder placeholder for upcoming business logic

Closes Week 1 AC 6.
* feat(api): add POST /api/v1/events with prisma-zod schemas

Adds the first Route Handler: parse -> Zod -> server -> Prisma.
Schemas in server/schemas/ derived from prisma-zod-generator
output. PrismaClient singleton uses @prisma/adapter-pg
(required by Prisma 7).

Closes Week 1 AC 7.

* feat(api): add CORS for chrome-extension origin

server/cors.ts: withCors() wrapper + corsPreflight() helper.
Echoes request Origin back if it matches the chrome-extension
ID pattern, sends "null" otherwise. POST /api/v1/events wrapped
+ OPTIONS handler exported.

Switch Zod error response to z.treeifyError (z.format deprecated).

Closes Week 1 AC 3.
MV3 + TypeScript scaffold under extension/. Background SW, content
script (<all_urls>), empty popup, placeholder icons. Loads in Chrome
via Load unpacked. See plans/AC4-extension-init.md.
…C 5) (#6)

* docs(readme): add full root readme (AC 5)

* chore(repo): add license, contributing guide, and PR template

* ci: add lint and typecheck workflow

GitHub Actions workflow that runs on every pull request and push
to development/main. Two jobs:

- dashboard: prisma generate (required before tsc — generated client is
  gitignored), npm run lint, tsc --noEmit
- extension: tsc --noEmit

* chore(lint): tighten typescript rules

Stricter eslint rules in dashboard to enforce CLAUDE.md type-safety
guarantees at PR time:

- @typescript-eslint/no-explicit-any: error
- @typescript-eslint/ban-ts-comment: error
- @typescript-eslint/consistent-type-assertions: forbid object-literal
  casts ({ foo } as T)
- no-restricted-syntax: forbid casting to unknown (incl. as unknown as T)

Also ignore generated/** (output of prisma generate — autogenerated)
and remove the now-redundant eslint-disable-next-line in server/db.ts.

* chore(tooling): add husky pre-commit, gitattributes, nvmrc

Pre-commit hook runs eslint --fix on staged dashboard files via
lint-staged. Catches issues locally before CI; typecheck stays in CI
because it needs the whole project and is too slow per-commit.

- husky + lint-staged installed in a new root package.json (the repo
  is a monorepo without a workspace tool)
- lint-staged.config.mjs uses npm --prefix dashboard to run eslint
  from the dashboard cwd so the flat config resolves naturally
- .gitattributes normalizes line endings to LF — stops the
  "LF will be replaced by CRLF" warnings on every commit
- .nvmrc pins Node 20 for nvm use / IDE auto-switch

* fix(ci): provide dummy DATABASE_URL for prisma generate

prisma.config.ts uses prisma/config's strict env() helper, which
throws PrismaConfigEnvError if DATABASE_URL is missing at load time.
The CI runner has no .env file (gitignored), so prisma generate
failed before reading the schema.

prisma generate does not connect to the database — it only reads the
schema and writes the generated client + zod schemas. A dummy URL
satisfies the config loader without touching any real database.

Set DATABASE_URL only on the prisma generate step, not on the whole
job, so an accidental query elsewhere still fails loudly.
* docs(plans): add week 2 AC 1 auth plan

* feat(dashboard): add auth server module and /api/auth/extension route

* feat(extension): implement google oauth flow, jwt storage and auth popup
…+3) (#8)

* docs(plans): add week 2 AC 2+3 content script and session manager plan

* feat(extension): add content script page metadata parser (AC 2)

* feat(extension): add session manager with start/stop/timer and storage (AC 3)

* docs(plans): update AC 2+3 plan with identified gaps for AC 4 and AC 5
… (Week 2 AC 4) (#9)

* docs(plans): add week 2 AC 4 popup UI plan

* feat(extension): add session pause/resume to session manager (Gap 1 from AC 3)

* feat(extension): implement popup UI with session controls and timer (AC 4)

* docs(backlog): add email and password auth plan with smtp verification

* feat(extension): add logout button and note save feedback to popup

* feat(extension): add dev mode fake auth bypass via VITE_DEV_MODE
* docs(plans): add week 2 AC 5 data sync plan

* feat(dashboard): add requireUser JWT helper and protect /api/v1/events

Verifies Bearer token via JWT_SECRET, checks session ownership on POST.

* feat(dashboard): add POST and PATCH /api/v1/sessions endpoints

POST creates a session for the authenticated user. PATCH /:id sets endedAt, scoped to the user's own sessions.

* feat(extension): create DB session on SESSION_START with best-effort retry

Adds dbSessionId to local Session, fire-and-forget POST /api/v1/sessions on start and PATCH on stop. Failures left for sync alarm to retry.

* feat(extension): normalize pending event payload shape

PageMetadata flattens to content via metaDescription + headings. Notes get worktrace://note/<uuid> URL so z.url() passes without schema changes.

* feat(extension): add batched event sync with alarm and retry

Every 30s a chrome.alarms tick flushes up to 50 queued events. Final flush also fires on SESSION_STOP. 4xx drops poison events, 401 stops until re-login, network and 5xx keep the queue for the next tick.

* feat(extension): show sync status in popup

Adds a SYNC_GET_STATUS message that returns lastSyncedAt, lastError and queueSize. Popup renders a second line under the queue count showing time since last sync or the error message.

* chore(dashboard): pin turbopack root to dashboard directory

Two lockfiles (root husky/lint-staged and dashboard) made Turbopack infer the repo root as workspace root, watching extension and unrelated paths and slowing dev. Setting turbopack.root scopes the watcher.

* feat(extension): show logged-in user email in popup header

AUTH_GET_STATUS now returns the email decoded from the JWT payload. Popup renders it under the logo while authenticated, hides on logout.
…nedMs (#11)

* docs(plans): add AC6 music capture plan

* feat(extension): add TrackInfo types and music message definitions

* feat(extension): add YouTube Music content script with MutationObserver

* feat(extension): add SoundCloud content script with MutationObserver

* feat(extension): update manifest with music content scripts and host_permissions

* feat(extension): handle TRACK_CAPTURED in background and save to DB

* feat(dashboard): add POST /api/v1/tracks route handler

* feat(extension): display now-playing track in popup UI

* fix(extension): skip sync flush when unauthenticated to stop retry flood

* fix(extension): fix track section visibility and show auth error in popup

* fix(extension): add TRACK_REQUEST on-demand pull to fix SW race condition

* docs: add local dev checklist to CLAUDE.md

* fix(extension): save track to DB on on-demand TRACK_REQUEST pull

* feat(extension): show track listening duration timer in popup

* docs(plans): update AC6 plan with actual commits and bug log

* feat(db): add endedAt to Track model for listen duration analytics

* feat(dashboard): add PATCH /api/v1/tracks/:id to record track endedAt

* feat(extension): track endedAt per song via currentDbTrackId and PATCH on change or session stop

* feat: accumulate listenedMs, pause track timer, gray out music block on pause

* fix(extension): detect music pause via .time-info DOM polling on YTM

Use ytmusic-player-bar .time-info textContent instead of broken slider
aria-valuenow (returned a static value). Popup now freezes the track
duration timer when the song is paused.

* docs(plans): finalize AC6 plan — mark all commits done, add commit 16 YTM pause fix
* docs(plans): add AC7 privacy blocklist plan

* feat(extension): blocklist storage module and background message handlers

* feat(extension): filter blocked domains in content scripts before sending

* feat(extension): add privacy blocklist section to popup

* fix(extension): accordion not closing - isAccordionOpen state + CSS hidden override
… AC8) (#13)

* docs(backlog): music track duration display bug — popup-local state lost on reopen

* docs(plans): add AC8 parsing modes plan

* feat(extension): parsing mode storage module and background message handlers

* feat(extension): apply parsing mode in content script metadata collection

* feat(extension): add parsing mode segmented control to popup

* fix(extension): write page events directly to storage to survive SW sleep

* fix(extension): freeze track timer on session pause, restore correctly on popup reopen

Closes the music track duration display bug from backlog. Three independent
issues, three priorities in the new initDurationFromStorage:

  P1 — session paused/stopped: freeze at the last snapshot value, ignore any
  delta. Session pause does not pause the music player, so DOM playbackTime
  keeps advancing in the YTM/SoundCloud tab — we must not count that.

  P2 — session active + matching snapshot: compute delta from the snapshot's
  saved playbackTime to the current DOM playbackTime. The song's own playback
  position is the source of truth for "did music actually play while popup was
  closed" — it only advances when music is playing.
    delta > 0  → add deltaSec*1000 to snapshot.displayDurationMs
    delta = 0  → music was paused in the player → keep snapshot value
    delta < 0  → user seeked backward → keep snapshot value (best effort)

  P3 — fallback: background-accumulated trackListenedMs (correct for session
  pause path through endCurrentDbTrack, best-effort otherwise).

Storage additions:
  - trackListenedMs: accumulated by background in endCurrentDbTrack on every
    session pause / track change. Reset to 0 in TRACK_CAPTURED, removed in
    SESSION_STOP.
  - popupSnapshot { trackKey, playbackTime, displayDurationMs, capturedAt }:
    written every popup tick in refreshDuration. Removed in TRACK_CAPTURED and
    SESSION_STOP so a stale snapshot from a previous track never wins.

Popup changes:
  - applyTrack: drop capturedAt-based initialisation, set needsStorageInit=true
    on track change so the next refreshTrack reads from storage.
  - refreshTrack: one-shot await initDurationFromStorage when flag is set.
  - parsePlaybackTime: handles M:SS and H:MM:SS DOM formats.

Known edge cases (acceptable as best-effort, DB listenedMs remains accurate):
  - Song looped/replayed → playbackTime resets to 0, second play undercounts.
  - Popup closed longer than song length → playbackTime caps, undercounts.
…eware guard (#14)

* docs(plans): add week 3 AC 1 dashboard auth plan

* feat(dashboard): add web google login + logout routes and session cookie helper

* feat(dashboard): add /login page with google identity services button

* feat(dashboard): add middleware guarding /dashboard and placeholder page

* fix(dashboard): force english locale for google sign-in button

* chore(dashboard): remove default next.js boilerplate and set worktrace metadata
* docs(plans): add week 3 AC 2 event feed plan

* style(dashboard): port extension color tokens and add sticky app header

* feat(dashboard): allow cookie auth in requireUser and add GET /api/v1/events

* feat(dashboard): event feed with infinite-scroll cursor pagination

* feat(dashboard): add events-per-day and top-tags charts with aggregated stats endpoint

* style(dashboard): add cursor-pointer + hover states on filters, logout, cards + future-date guard

* feat(dashboard): top-3 sessions panel with per-host duration breakdown

- adds GET /api/v1/sessions/top route + getTopSessionsForUser server module
- per-event duration computed via LEAD window function over (sessionId, timestamp)
- last event of a session contributes 0s so totals = max(ts) - min(ts), gaps capped at 30 min
- top host per session via DISTINCT ON ordered by host duration
- renders alongside charts in a 3-col grid; client component prefetched on /dashboard

* feat(dashboard): match extension favicon and add seo metadata

- adopt extension icon-128.png as app/icon.png + apple-icon.png (Next App Router convention)
- drop default favicon.ico
- root metadata: title template, description, OG and Twitter cards, theme color
- /login and /dashboard get page-specific titles and are excluded from index/follow

* style(dashboard): widen content container and vertically center events-per-day chart

* docs(plans): add stale-session cleanup to backlog

Discovered during Week 3 AC 2 — leftover sessions (endedAt=NULL after browser
close / SW sleep) can dominate the TOP 3 panel even when nominally 'active'.
Other metrics (byDay, topTags, host durations) compute correctly; only
session ranking is affected. Documents 4 mitigation strategies, recommends
Vercel Cron route for cleanup.

* refactor(dashboard): share getCurrentUser helper and useClientLocalized hook

* chore(dashboard): zod-validate api responses, align page size, scope top-sessions to 90d

* docs(plans): backlog jwt unification and post-logout cache reset
… sampling, and API key management (#16)

* docs(plans): add week 3 AC 3 ai session report plan

* feat(dashboard): add UserSetting model and encrypted groq api key endpoints

* feat(dashboard): add groq client and report context builder with token-budget sampling

* feat(ai): add reports/generate route and /dashboard/reports page with markdown render and download

* feat(reports): API key modal portal + fix Groq 413 token budget

Lower INPUT_TOKEN_BUDGET 30k→7.5k so context fits Groq free-tier 12k TPM
limit; add GroqContextLimitError (413) with user-facing message. Wire up
ApiKeyModal (createPortal) with save/clear flow on /dashboard/reports.

* fix(ui): chips+actions same row, burger on far right, wider logo gap
…t.js (#17)

* docs(plans): add week 3 AC4 music analytics plan

* refactor(server): extract resolveRange to server/range.ts

* feat(music): server module, types, and GET /api/v1/music/stats route

* feat(music): /dashboard/music page with Chart.js charts

* feat(nav): add MUSIC to header nav

* fix(music): aggregate getTopTracks by artist+title to deduplicate rows

* fix(music): correct logic in music queries

- getTopArtists: COUNT(DISTINCT title) via $queryRaw (was counting rows, not unique tracks)
- getMusicProductivity: GROUP BY artist+title + SUM(listenedMs); endedAt null fallback uses capturedAt+listenedMs duration instead of NOW()
- getMusicTotals: totalTracks counts distinct (artist,title) pairs via groupBy (was counting all capture rows)
…cations, rate limit handling (#18)

* docs(plans): AC5 error handling plan

* feat(server): add typed API error helper and standardize route handlers

* feat(dashboard): add toast notification system

* fix(reports): handle 429 rate limit with retry time + tighten token budget
…ehide (#19)

* docs(plans): stale data cleanup plan — sessions + music timer

Moves backlog item to week4, adds stale music timer problem (680min bug),
resolves all open questions, switches to GitHub Actions cron.

* feat(server): closeStaleSessions and closeStaleTracksForClosedSessions

* feat(dashboard): GET /api/cron/close-stale route, protected by CRON_SECRET

* chore(ci): GitHub Actions cron to close stale sessions every 15 min

* fix(extension): send TRACK_STOP on pagehide to close stale music timers
* docs(plans): move post-week3 tech debt plan to week4

* refactor(server): port issueJWT and requireUser to jose

* chore(deps): remove jsonwebtoken and @types/jsonwebtoken

* fix(dashboard): clear TanStack cache on logout

* chore(plans): drop email-password-auth from backlog, out of scope for Week 4
…README (#21)

* docs(plans): add week4 deploy-polish plan

* chore(extension): add zip script via PowerShell Compress-Archive

* docs(readme): update tech stack, zip instructions, remove stale Week 1 notes

* chore(ci): auto GitHub Release with extension zip on merge to main
…tive background (#22)

* docs: add framer-motion animations plan

* chore: install framer-motion

* feat(anim): scroll-reactive timers bg, prism wave, Lenis smooth scroll, page transitions

* feat(anim): AI report loading panel + modal spring entrance

* feat(anim): event feed stagger entrance + card hover lift

* feat(anim): nav indicator layoutId, mobile-nav spring, progress bars animate

* feat(anim): toasts slide-in, music count-up stats, login stagger entrance

* fix(anim): style report + music metadata as pill badges for readability

* feat(anim): charts fade-in on data load via AnimatePresence
* feat(reports): localStorage cache per range, auto-load on switch, createdAt badge

* feat(account): delete account flow — animated 2-step modal + DELETE /api/auth/delete-account

* fix(reports): scope cache key by normalized user email to prevent cross-account bleed

* fix(account): escape key, error display, localStorage cleanup on logout/delete, hydration fix

* fix(account): SSR-safe portal via typeof document check, remove dead logout-button.tsx
…ening (#24)

* fix(extension): unify JWT decode into single decodeJwtPayload helper

* fix(extension): log ensureDbSession failure instead of silent void

* fix(extension): extract isTrackBlocked helper, remove blocklist check duplication

* fix(extension): exhaustiveness guard on message type dispatcher

* feat(extension): make WORKTRACE title a link to the dashboard

* fix(dashboard): surface localStorage QuotaExceededError as toast in report cache

* fix(dashboard): validate Groq key gsk_ prefix before save

* feat(dashboard): add footer with GitHub releases link

* fix(extension): guard flush() against concurrent runs to prevent duplicate events
@vercel

vercel Bot commented Jun 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worktrace Ready Ready Preview, Comment Jun 2, 2026 7:14pm

@BODMAT BODMAT merged commit b4cce4e into main Jun 2, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant