Commit 7030bea
* fix: consolidated triage cleanup — address all open findings from PRs #61/62/63/65
Addresses every actionable bug from 4 stale morning-triage PRs plus 2 critical
visible regressions (LI-236, LI-322). 31 files changed, 0 lint errors, 104/104
tests pass, production build clean.
## Security & server (5)
- next.config.mjs — add Content-Security-Policy header (9 directives: default/
script/style/img/font/connect/frame-ancestors/base-uri/form-action)
- lib/partnerships.js — add UUID validator + guards on all 5 .or() call sites;
batch notifyPartner N+1 inserts into single .insert([...]) call
- lib/streaks-advanced.js — delete useStreakFreeze alias (ESLint hooks rule);
add xp_events-based idempotency for streak-freeze milestone rewards
- lib/email.js — add timeZone: 'UTC' + "(UTC)" suffix to deadline formatting
- app/dashboard/pacts/PactsPageClient.js — defense-in-depth .eq('user_id')
on pact delete
## Component logic (10)
- components/Sidebar.js — SSR guard on localStorage access
- components/FocusTimer.js — hoist defaultPalette to module scope, drop from
useMemo deps
- components/Toast.js — use crypto.randomUUID() for toast IDs
- components/PactCard.js — remove `now` var shadow; normalize deadline day
math to local midnight so "due today" is timezone-stable
- components/TodayBar.js — resolve user IANA timezone and pass to
checkStreakAtRisk + applyStreakFreeze
- components/ActivityFeed.js — dedup loadMore append via Set of existing IDs
- lib/FocusContext.js — remove redundant useEffect for handleTimerCompleteRef
- app/dashboard/DashboardClient.js — reduce pacts limit from 200 to 50
(dashboard renders at most 3)
- app/dashboard/DashboardLayout.js — log timezone-sync errors instead of
swallowing them
- components/DailySummaryCard.{js,module.css} — deleted (dead code replaced
by TodayBar in dashboard redesign)
## Dark mode & visual regressions (5)
- components/PactCard.module.css — add full dark coverage for base .card,
:hover, .title/.description/.deadline text, all action buttons, badges
(fixes LI-322: pact cards rendering as empty dark rectangles)
- components/TaskCard.module.css — wrap bare [data-theme="dark"] selector in
:global() (was broken in CSS Modules) + add .card/.actions dark overrides
- components/CreatePactModal.module.css — full dark coverage (overlay, modal,
inputs, buttons, template picker) with glass background
- components/MonthlyCalendar.module.css — replace hardcoded #FFFFFF with
var(--text-inverse) for dark-mode calendar cells
- components/CompactActivityCard.js — replace inline Framer Motion with
fadeInUp + cardHover presets from @/lib/animations
## Landing page LI-236 (1)
- components/LandingPageClient.js — fix sections below hero rendering as
blank dark void. Change initial state of 13 top-level whileInView configs
from { opacity: 0 } → { opacity: 1 } so content paints on initial load
even if IntersectionObserver fails to fire. Extract sectionViewport
(amount: 0.05 + -10% margin) and sectionFadeInSafe constants. Verified
with DOM inspection: hero/appPreview/featuresDark/howItWorks/ctaFooter
all render at opacity 1.
## Micro-fixes & accessibility (6)
- components/CreateGroupModal.js — maxLength={100} on group name; replace
modular-bias `% 36` invite-code generation with rejection sampling
- components/CreateTaskModal.js — aria-label="Task title"
- components/NotificationBell.js — replace 25-frame rAF polling loop with
single position calc + ResizeObserver; fix pre-existing
react-hooks/set-state-in-effect on wiggle trigger via rAF defer
- components/CreatePactModal.js — inline whileHover/Tap → buttonHover/
buttonTap presets
- app/share/streak/page.js — sanitize searchParams (parseInt streak, strip
<>"'& from name, cap 50 chars); remove unused prop
- tests/landing.spec.js — scope Features/HowItWorks locators to
nav:not([aria-label="Footer navigation"]) to resolve Playwright strict-
mode collision with footer nav
## Test updates
- tests/unit/lib/streaks-advanced.test.js — useStreakFreeze → applyStreakFreeze
Closes PRs #61, #62, #63, #65 (all superseded by this consolidated fix set).
Closes Notion LI-236, LI-322, and 18 other verified open findings.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: XP system + Groups card sizing + streak RPCs
## XP / Streak fix (root cause: gamification trigger blocked all client writes)
The protect_gamification_cols trigger on profiles was firing for any session
role != service_role — silently overwriting NEW.total_xp := OLD.total_xp and
related gamification columns. Client-side writes were no-op'd. Vayun's profile
was stuck at 930 XP / streak 1 / last_activity_date 2026-04-11 despite logged
xp_events totaling 735.
### Server (applied via migrations)
- Fixed trigger to honor a session-var bypass (`app.skip_gamification_trigger`)
- Updated `award_xp` RPC to set the bypass flag before UPDATE
- Added `update_streak_activity(uuid, int, int, date)` RPC for streak writes
- Added `consume_streak_freeze(uuid, date)` RPC for freeze use (updates
last_activity_date + cooldown atomically)
- Added `award_streak_freeze(uuid, int, int)` RPC for milestone rewards
### Client
- `lib/FocusContext.js` — award 5 XP on focus session completion (was missing)
- `components/TaskCard.js` — award 8 XP on task -> 'done' (was missing)
- `lib/streaks-advanced.js` — use RPCs instead of direct profiles UPDATE:
- `updateStreakOnCompletion` → `rpc('update_streak_activity')`
- `applyStreakFreeze` → `rpc('consume_streak_freeze')` with local today date
- `awardStreakFreeze` → `rpc('award_streak_freeze')` for milestone rewards
- `lib/gamification.js` — log eventType/xpAmount/userId/pgCode on errors
(prefix `[awardXP] Failed`) so future silent failures are visible
- Removed unused imports (`formatUTCDate`) and dead code (`isSoundEnabled`
in FocusContext, `members` prop in TaskCard)
## Groups card sizing fix
The "hello" group with 0/20 tasks done was rendering at 2x width because:
- `GroupsPage.module.css` had `.groupCardHero { grid-column: 1 / -1 }`
- `GroupsPageClient.js` selected hero by `taskCount` (total), not completion
### Changes
- Hero selection now requires >= 5 completed tasks (`tasksDone`), not total
- Hero no longer spans full grid — consistent card sizes across rows
- Hero styling now: accent border-left + layered glow shadow + gradient
progress fill + "Most Active" lightning-bolt badge (inline SVG)
- Dark-mode override updated to match
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: live XP refresh + confetti z-index
## XP live refresh
`awardXP` now dispatches a `xp-updated` CustomEvent on success. `XPBar` listens
for that event and re-fetches — so the dashboard XP bar updates instantly on
pact/focus/task completion without a page reload. `Sidebar` and `MobileNav`
already listened for this event; they just never got the signal because
FocusContext/TaskCard XP paths had no broadcast. Centralizing the dispatch in
`lib/gamification.js` means every awardXP call everywhere gets live UI.
## Confetti
`useConfetti()` hook was missing `zIndex: 9999` — the other confetti helpers
(fireSideConfetti, fireStars, fireMilestoneConfetti) set it but this one
defaulted to 100, so any overlay/toast above it was hiding the bursts.
Added zIndex: 9999 to `useConfetti`, `fireConfetti`, and
`fireConfettiFromElement` for consistency.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(groups): cap sections to 3 with "Show all" toggle
Matches the dashboard activity pattern — don't scroll forever.
- Each section (Your Groups / Joined Groups) shows top 3 by default
- Sort by tasksDone desc, then memberCount desc (real-activity signal)
- "Show all N groups" / "Show less" toggle reveals the rest inline
- Hero card selection unchanged (requires tasksDone >= 5)
- New .showMoreBtn styled consistent with emptyActionSecondary +
full dark-mode override
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(groups): cap activity feed on group detail — no endless scroll
ActivityFeed now accepts `disableInfiniteScroll` prop. When true, the
IntersectionObserver-based auto-load is skipped in favor of an explicit
"Show more activity" button below the feed. Group detail page uses this
with `pageSize={5}` so the section doesn't balloon to hundreds of items
as the group gets busy.
Dashboard activity behaviour unchanged (still pageSize=3 with external
"View Older Activity" link to stats).
Also reverted the previous Groups-page pagination — that was a
misinterpretation of the request.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* revert: undo Groups-page pagination (misread request)
Revert the "Show all N groups" toggle on the Groups page — that was a
misinterpretation. The real ask was about the activity feed inside a
group detail page, already addressed in the previous commit.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(groups): bounded activity panel with internal infinite scroll
Instead of the manual "Show more" button, the group detail activity
panel is now a fixed-height block (matches the left column) with
infinite scroll inside. Matches the user's mock: one clean, organized
block — scroll through all activities without pushing page content down.
### How it works
- `.activityGrid` changed from `align-items: start` → `stretch`
- New `.activityPanel` wrapper: height: 100%, min-height: 500, max-height: 720
- `.container` (ActivityFeed): `min-height: 0` so flex can shrink
- `.feed`: `flex: 1; min-height: 0; overflow-y: auto;` + overscroll-contain
- IntersectionObserver's scroll root is already the feed ref, so
loadMore triggers as the user scrolls INSIDE the panel
- Mobile/< 1024px: reverts to `align-items: start` with a smaller max-height
ActivityFeed now uses pageSize={10} (default initial load). The
`disableInfiniteScroll` prop + manual show-more button are left in for
potential reuse (e.g. stats page) but no longer used here.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(e2e): align auth and landing assertions with current copy
Agent-Logs-Url: https://github.com/vayungodara/lockin/sessions/39b6f264-427a-46ba-b139-a4ebacca14cf
Co-authored-by: vayungodara <187050579+vayungodara@users.noreply.github.com>
* fix: address all Claude + Codex review comments on PR #66
## Critical fixes
### PactCard "Due tomorrow" regression (P0)
`formatDeadline` reverted from local-midnight day math to ms-based
hours-first math. Sub-24h deadlines crossing local midnight now
correctly render "Due in 2 hours" instead of "Due tomorrow" while the
card simultaneously pulses urgencyCritical red.
### Partnership notifications RLS (CRITICAL)
`lib/partnerships.js#notifyPartner` was batch-inserting rows with
`user_id: partnerId` — violated `WITH CHECK(auth.uid() = user_id)` on
every row. Now routes through new `notify_partner` SECURITY DEFINER
RPC that validates the caller is in an accepted partnership with each
recipient before inserting. Preserves the batched write; bypasses RLS
correctly.
### Streak idempotency marker RLS (Codex P1)
`updateStreakOnCompletion` direct insert into `xp_events` was blocked
by RLS (no client INSERT policy). Marker now routes through `awardXP`
→ `award_xp` RPC. The allowlist was extended with a prefix match for
`streak_freeze_milestone_*` event types. `xp_amount=0` so no actual
XP granted — pure idempotency marker.
### share/streak array-param TypeError (P2)
`generateMetadata` crashed on `?name=a&name=b` — arrays don't have
`.replace`. Coerce `searchParams.streak` and `searchParams.name` via
`Array.isArray` check before string operations.
### TaskCard XP farming exploit (Codex P2)
Toggling done → todo → done repeatedly awarded XP every cycle. Added
idempotency guard: `newStatus === 'done' && task.status !== 'done'`.
Also dropped `task.assignee_id` fallback (schema doesn't have that
column — grep confirmed).
### MonthlyCalendar legibility regression (Claude P1)
Reverted `#FFFFFF` → `var(--text-inverse)` on `.level3/.level4`
heatmap cells. In dark mode `--text-inverse` is `#08090D` — black on
medium-dark indigo is illegible. These cells have saturated accent
bgs in BOTH themes, so pure white is correct.
### CSP hardening (Claude P2)
Moved `'unsafe-eval'` behind `NODE_ENV === 'development'` gate.
Added `vercel.live` to script-src/connect-src/frame-src for preview
toolbar. Removed redundant `lh3.googleusercontent.com` (covered by
wildcard).
### Dashboard pacts limit truncation (Codex P2)
Bumped `.limit(50)` back to `.limit(200)` — active pacts could be
pushed out of the window by users with >50 historical
completed/missed pacts, breaking due-today/overdue counts.
### Sidebar localStorage SecurityError (Claude P3)
`getSidebarCollapsed` runs via `useSyncExternalStore`'s getSnapshot
— must be pure. Users with blocked third-party storage would throw
SecurityError before `.getItem` ran, crashing the entire Sidebar
subtree. Wrapped both getter and setter in try/catch.
### FocusContext ref mutation during render (Claude P3)
Restored useEffect pattern for `handleTimerCompleteRef` sync.
Inline render-phase ref writes are unsafe under React 18 concurrent
mode — discarded renders can leave the ref pointing at uncommitted
callbacks.
### NotificationBell dedup + sidebar-aware repositioning (P2 + Nit)
Removed duplicated `reposition` function — reuse `updatePosition`
directly. Added storage-event listener for `sidebar-collapsed` since
the Sidebar is `position: fixed` and ResizeObserver on document.body
never fires on sidebar toggle. Reposition happens 380ms after the
animation completes.
## Cleanup
### LandingPageClient dead motion wrappers (Claude P3)
After the LI-236 fix made 15+ sections start with opacity:1, the
`whileInView` targets were identical no-ops. Converted static section
containers to plain `<div>/<h2>/<p>` elements. Kept `motion.div` only
where real motion (e.g. `whileHover={cardHover}`) remains. Removed
`sectionFadeInSafe`, `sectionViewport`, `smoothTransition`.
### ActivityFeed dead disableInfiniteScroll prop (Claude P3)
Removed the prop + manual "Show more" button ternary since we moved
to "bounded container + internal infinite scroll". Dropped
`.showMoreBtn` CSS block.
## Tests
- Added RPC allowlist helper to supabase-mock (catches missing-RPC
regressions going forward)
- 3 new tests for streak-freeze milestone idempotency:
1. Awards on first milestone hit + inserts marker
2. Skips award on second call (marker exists)
3. Skips entirely on non-milestone day
- Total: 107/107 passing (was 104)
## Repo sync
Committed `supabase/gamification_hardening_2026_04_13.sql` — source
of truth for all new RPCs (award_xp, update_streak_activity,
consume_streak_freeze, award_streak_freeze, notify_partner) plus the
protect_gamification_columns trigger bypass. Previously applied only
to production via Supabase migrations; now tracked in the repo.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: align landing e2e with current copy
Co-authored-by: vayungodara <187050579+vayungodara@users.noreply.github.com>
* fix: address all 5 follow-up review comments on PR #66
Bot reviewers caught issues introduced in the previous fix push.
## CRITICAL — notify_partner status mismatch
The new `notify_partner` RPC validated against `ap.status = 'accepted'`
but `lib/partnerships.js:93` writes `'active'` for accepted requests.
Every partner notification was silently a no-op. Fixed RPC to check
`'active'`. SQL source of truth + production migration both updated.
## HIGH — TaskCard XP recipient broken for group owners
`recipientId = task.owner_id || task.created_by || currentUser?.id`
sent the award to the task owner first, but `award_xp` enforces
`auth.uid() = p_user_id`. When a group owner closed someone else's
task the award was rejected. Award now goes to `currentUser.id`
(the actor who actually completed the work). Semantically more
correct AND it actually succeeds.
## HIGH — Streak idempotency marker permanent
`event_type = streak_freeze_milestone_7` was a global marker, so a
user who broke their streak and rebuilt back to day 7 a month later
would never earn another freeze. Marker now includes the streak's
START date (`streak_freeze_milestone_7_2026-04-08`), so each unique
streak run earns its own freeze. Test updated to match the regex
pattern.
## MEDIUM — award_streak_freeze server-side milestone check
Any authed user could call the RPC directly via the Supabase JS
client to bump their freezes by up to MAX_FREEZES (5). RPC now reads
`profiles.current_streak` server-side and rejects calls when the user
is not on a known milestone (7/14/30/60/90/180/365). Also tightened
`p_amount` to exactly 1.
## MEDIUM — consume_streak_freeze server-side cooldown
The 3-day cooldown was only checked client-side in
`lib/streaks-advanced.js:111-124`. RPC now re-checks
`streak_freeze_last_used` server-side and returns a clear error if
within the cooldown window. Direct API callers can no longer drain
the entire pool in one session.
## Tests
107/107 still passing. Idempotency test updated to match new
date-scoped marker format.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: vayungodara <187050579+vayungodara@users.noreply.github.com>
Co-authored-by: openai-code-agent[bot] <242516109+Codex@users.noreply.github.com>
1 parent 04c0522 commit 7030bea
41 files changed
Lines changed: 1271 additions & 735 deletions
File tree
- app
- dashboard
- groups
- [id]
- pacts
- components
- lib
- supabase
- tests
- setup
- unit/lib
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
86 | 92 | | |
87 | 93 | | |
88 | 94 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
38 | 40 | | |
39 | 41 | | |
40 | 42 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
90 | | - | |
| 90 | + | |
91 | 91 | | |
92 | | - | |
93 | 92 | | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
94 | 141 | | |
95 | 142 | | |
96 | 143 | | |
| 144 | + | |
97 | 145 | | |
98 | 146 | | |
99 | 147 | | |
| |||
435 | 483 | | |
436 | 484 | | |
437 | 485 | | |
438 | | - | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
439 | 500 | | |
440 | 501 | | |
441 | 502 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
38 | 46 | | |
39 | 47 | | |
40 | 48 | | |
| |||
115 | 123 | | |
116 | 124 | | |
117 | 125 | | |
118 | | - | |
119 | | - | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
120 | 129 | | |
121 | | - | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
122 | 134 | | |
123 | 135 | | |
124 | 136 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
456 | 456 | | |
457 | 457 | | |
458 | 458 | | |
459 | | - | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
460 | 477 | | |
461 | 478 | | |
462 | 479 | | |
463 | 480 | | |
464 | 481 | | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
465 | 488 | | |
466 | 489 | | |
467 | 490 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
434 | 434 | | |
435 | 435 | | |
436 | 436 | | |
437 | | - | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
438 | 440 | | |
439 | 441 | | |
440 | 442 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
66 | 66 | | |
67 | 67 | | |
68 | 68 | | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
69 | 72 | | |
70 | 73 | | |
71 | 74 | | |
72 | | - | |
| 75 | + | |
| 76 | + | |
73 | 77 | | |
74 | 78 | | |
75 | 79 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
8 | | - | |
9 | | - | |
10 | | - | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
11 | 17 | | |
12 | 18 | | |
13 | 19 | | |
| |||
18 | 24 | | |
19 | 25 | | |
20 | 26 | | |
21 | | - | |
| 27 | + | |
22 | 28 | | |
23 | 29 | | |
24 | 30 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
135 | 135 | | |
136 | 136 | | |
137 | 137 | | |
138 | | - | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
139 | 142 | | |
140 | 143 | | |
141 | 144 | | |
| |||
237 | 240 | | |
238 | 241 | | |
239 | 242 | | |
240 | | - | |
| 243 | + | |
241 | 244 | | |
242 | 245 | | |
243 | 246 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| |||
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
34 | 42 | | |
35 | 43 | | |
36 | 44 | | |
| |||
0 commit comments