Skip to content
8 changes: 4 additions & 4 deletions components/ActivityComments.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,21 @@ export default function ActivityComments({ activityId, initialCount = 0 }) {
{comments.map(comment => (
<div key={comment.id} className={styles.comment}>
<div className={styles.commentAvatar}>
{comment.user.avatar_url ? (
{comment.user?.avatar_url ? (
<Image
src={comment.user.avatar_url}
alt={comment.user.full_name}
alt={comment.user?.full_name || 'User'}
width={20}
height={20}
className={styles.commentAvatarImg}
/>
) : (
<span>{(comment.user.full_name || 'U').charAt(0)}</span>
<span>{(comment.user?.full_name || 'U').charAt(0)}</span>
)}
</div>
<div className={styles.commentBody}>
<div className={styles.commentMeta}>
<span className={styles.commentAuthor}>{comment.user.full_name}</span>
<span className={styles.commentAuthor}>{comment.user?.full_name || 'Unknown'}</span>
<span className={styles.commentTime}>{formatRelativeTime(comment.created_at)}</span>
</div>
<p className={styles.commentText}>{comment.comment_text}</p>
Expand Down
2 changes: 1 addition & 1 deletion components/CreateGroupModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export default function CreateGroupModal({ isOpen, onClose, onGroupCreated }) {
if (err.code === '23505') {
setError('A group with this invite code already exists. Please try again.');
} else {
setError(err.message || 'Failed to create group. Please try again.');
setError('Failed to create group. Please try again.');
}
} finally {
setIsLoading(false);
Expand Down
6 changes: 0 additions & 6 deletions components/CreatePactModal.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -149,12 +149,6 @@
color-scheme: dark;
}

@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .input[type="date"],
:root:not([data-theme="light"]) .input[type="time"] {
color-scheme: dark;
}
}

.deadlineRow {
display: grid;
Expand Down
2 changes: 1 addition & 1 deletion components/CreateTaskModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export default function CreateTaskModal({ isOpen, onClose, onTaskCreated, groupI
onClose();
} catch (err) {
console.error('Error creating task:', err);
setError(err.message || 'Failed to create task. Please try again.');
setError('Failed to create task. Please try again.');
} finally {
setIsLoading(false);
}
Expand Down
2 changes: 1 addition & 1 deletion components/JoinGroupModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export default function JoinGroupModal({ isOpen, onClose, onGroupJoined }) {
onClose();
} catch (err) {
console.error('Error joining group:', err);
setError(err.message || 'Failed to join group. Please try again.');
setError('Failed to join group. Please try again.');
} finally {
setIsLoading(false);
}
Expand Down
1 change: 1 addition & 0 deletions components/NotificationBell.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default function NotificationBell() {
useEffect(() => {
if (unreadCount > prevUnreadRef.current) {
setWiggle(true);
prevUnreadRef.current = unreadCount;
const timer = setTimeout(() => setWiggle(false), 600);
return () => clearTimeout(timer);
}
Expand Down
5 changes: 0 additions & 5 deletions components/Sidebar.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@
background: linear-gradient(180deg, var(--surface-1), var(--bg-primary));
}

@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) .sidebar {
background: linear-gradient(180deg, var(--surface-1), var(--bg-primary));
}
}

.header {
display: flex;
Expand Down
136 changes: 136 additions & 0 deletions docs/reports/db-health-2026-04-02.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# LockIn DB Health — 2026-04-02

**Generated:** 2026-04-02 (automated)
**Status:** WARNINGS

---

## RLS Security
| Table | RLS Enabled | Policies | Status |
|-------|-------------|----------|--------|
| accountability_partnerships | Yes | 4 (SELECT, INSERT, UPDATE, DELETE) | OK |
| activity_comments | Yes | 3 (SELECT, INSERT, DELETE) | OK |
| activity_log | Yes | 2 (SELECT, INSERT) | OK |
| activity_reactions | Yes | 3 (SELECT, INSERT, DELETE) | OK |
| focus_sessions | Yes | 3 (SELECT, INSERT, UPDATE) | OK |
| group_challenges | Yes | 4 (SELECT, INSERT, UPDATE, DELETE) | OK |
| group_members | Yes | 3 (SELECT, INSERT, DELETE) | OK |
| groups | Yes | 4 (SELECT, INSERT, UPDATE, DELETE) | OK |
| notifications | Yes | 4 (SELECT, INSERT, UPDATE, DELETE) | OK |
| nudges | Yes | 2 (SELECT, INSERT) | OK |
| pacts | Yes | 4 (SELECT, INSERT, UPDATE, DELETE) | OK |
| profiles | Yes | 3 (SELECT, INSERT, UPDATE) | OK |
| reminder_logs | Yes | 1 (SELECT) | OK |
| tasks | Yes | 4 (SELECT, INSERT, UPDATE, DELETE) | OK |
| user_achievements | Yes | 2 (SELECT, INSERT) | OK |
| user_onboarding | Yes | 3 (SELECT, INSERT, UPDATE) | OK |
| xp_events | Yes | 1 (SELECT) | OK |

**All 17 tables have RLS enabled with at least 1 policy.**

### SECURITY DEFINER Functions (10 total)
| Function | Risk | Notes |
|----------|------|-------|
| award_xp | MEDIUM | Grants XP — should verify caller is authenticated |
| can_nudge_user | LOW | Read-only check |
| create_notification | MEDIUM | Creates records — should verify caller |
| get_group_by_invite_code | LOW | Read-only lookup |
| get_user_group_ids | MEDIUM | Used in RLS policies — if compromised, bypasses access control |
| handle_new_user | LOW | Triggered on auth signup |
| is_group_owner | LOW | Used in RLS policies — read-only |
| mark_all_notifications_read | MEDIUM | Modifies data — should verify caller |
| notify_group_on_member_join | LOW | Triggered by group join |
| notify_on_reaction | LOW | Triggered by reaction insert |

## Table Sizes
| Table | Row Count | Notes |
|-------|-----------|-------|
| accountability_partnerships | 0 | Feature unused |
| activity_comments | 2 | Low usage |
| activity_log | 301 | Main feed |
| activity_reactions | 2 | Low usage |
| focus_sessions | 52 | Active |
| group_challenges | 0 | Feature unused |
| group_members | 16 | Active |
| groups | 12 | Active |
| notifications | 8 | Active |
| nudges | 6 | Low usage |
| pacts | 22 | Active |
| profiles | 15 | Active users |
| reminder_logs | 0 | No reminders sent |
| tasks | 27 | Active |
| user_achievements | 5 | Active |
| user_onboarding | 10 | Active |
| xp_events | 59 | Active |

**Total rows: 537**

## Orphaned Rows
| Relationship | Count | Status |
|-------------|-------|--------|
| group_members -> groups | 0 | OK |
| tasks -> groups | 0 | OK |
| activity_log -> profiles | 0 | OK |
| activity_reactions -> activity_log | 0 | OK |
| focus_sessions -> profiles | 0 | OK |
| notifications -> profiles | 0 | OK |
| xp_events -> profiles | 0 | OK |
| user_achievements -> profiles | 0 | OK |
| nudges -> profiles (from_user_id) | 0 | OK |
| activity_comments -> activity_log | 0 | OK |

**All 10 relationships clean — no orphaned rows.**

## Stale Data
| Check | Count | Status |
|-------|-------|--------|
| Abandoned focus sessions (>24h) | 0 | OK |
| Notifications older than 90 days | 0 | OK |
| Activity log older than 90 days | 0 | OK |
| Unread notifications older than 30 days | 0 | OK |
| Reminder logs older than 30 days | 0 | OK |

**No stale data detected.**

## Anomalies
| Check | Count | Status |
|-------|-------|---------|
| Negative XP profiles | 0 | OK |
| Duplicate group memberships | 0 | OK |
| Past-deadline active pacts | 1 | WARNING |
| Streaks > 365 days | 0 | OK |
| Focus sessions > 24 hours | 0 | OK |
| Groups with 0 members | 2 | WARNING |
| Activity log with null user_id | 0 | OK |
| **XP drift** | **4 profiles** | **WARNING** |

### XP Drift Details
| User | Profile XP | Events XP | Drift |
|------|-----------|-----------|-------|
| Vayun Godara | 710 | 485 | +225 |
| LockIn Sentinel | 185 | 75 | +110 |
| Vayun Chintha | 45 | 0 | +45 |
| Aarav Godara | 45 | 0 | +45 |

Drift means `profiles.total_xp` exceeds `sum(xp_events.xp_gained)`. Likely caused by XP awarded before xp_events tracking was added, or by direct profile updates bypassing the events table.

## Suggested Cleanup SQL
```sql
-- DO NOT RUN. For manual review only.

-- Fix past-deadline active pacts (mark as missed)
-- UPDATE pacts SET status = 'missed' WHERE deadline < now() AND status = 'active';

-- Remove empty groups (0 members)
-- DELETE FROM groups WHERE id NOT IN (SELECT DISTINCT group_id FROM group_members);

-- Reconcile XP drift (option: insert correction xp_events)
-- INSERT INTO xp_events (user_id, event_type, xp_gained, metadata)
-- SELECT p.id, 'xp_reconciliation', p.total_xp - COALESCE(xe.total, 0), '{"reason": "historical drift correction"}'
-- FROM profiles p
-- LEFT JOIN (SELECT user_id, sum(xp_gained) as total FROM xp_events GROUP BY user_id) xe ON xe.user_id = p.id
-- WHERE p.total_xp != COALESCE(xe.total, 0);
```

## Errors
- Initial queries used wrong column names (`sender_id` -> `from_user_id`, `read` -> `is_read`, `xp` -> `total_xp`, `display_name` -> `full_name`, `xp_amount` -> `xp_gained`). Fixed and re-run successfully.
105 changes: 105 additions & 0 deletions docs/reports/frontend-audit-2026-04-02.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# LockIn Frontend Audit — 2026-04-02

**Generated:** 2026-04-02 (automated)
**Pages scored:** 9
**Visual audit context:** yes (22 Notion findings from visual-audit/competitor-intel/design-trend)

## Page Scores
| Page | Layout | Typography | Color | Components | Motion | Dark Mode | Responsive | Overall |
|------|--------|------------|-------|------------|--------|-----------|------------|---------|
| Landing | 8 | 7 | 7 | 8 | 8 | 5 | 8 | 7 |
| Dashboard | 8 | 8 | 8 | 7 | 7 | 6 | 7 | 7 |
| Pacts | 7 | 7 | 8 | 7 | 7 | 5 | 7 | 7 |
| Groups | 7 | 7 | 8 | 7 | 6 | 5 | 7 | 7 |
| Group Detail | 7 | 7 | 6 | 7 | 6 | 4 | 7 | 6 |
| Focus | 8 | 8 | 7 | 8 | 7 | 5 | 7 | 7 |
| Stats | 7 | 7 | 7 | 7 | 7 | 5 | 7 | 7 |
| Settings | 8 | 8 | 8 | 8 | 5 | 5 | 8 | 7 |
| Share Streak | 7 | 7 | 7 | 6 | 5 | 4 | 7 | 6 |

**Average: 6.8/10** — Lowest scores in Dark Mode (4.9 avg) and Group Detail/Share Streak pages.

## AI-Generated Tells
| File | Element | What a Human Designer Would Do Differently |
|------|---------|-------------------------------------------|
| page.module.css | .screenshotPlaceholder | 400px gray box "Your dashboard, visualized" — replace with actual screenshot or remove |
| Dashboard.module.css | .statsGrid | Uniform 3-col stat cards — vary card prominence, make streak larger |
| PactsPage.module.css | .pactsGrid | Cookie-cutter 2-col cards — add featured/pinned treatment for today's pacts |
| GroupsPage.module.css | .groupsGrid | All same size — active groups should feel "alive" with bigger cards |
| LandingPageClient.js:399 | avatarStack | Hardcoded hex colors for fake avatars — use real avatars or gradient placeholders |
| page.module.css | .step::after | Connecting lines between steps — very common AI pattern |
| SettingsPage.module.css | .settingCard | Uniform rows with toggles — group with section cards, add icons |
| StatsPage.module.css | .streakCard | Symmetric streak display — make primary number dramatically larger |

## Hardcoded Values (21 instances)
| File | Line | Current Value | Recommended CSS Variable |
|------|------|---------------|--------------------------|
| FocusTimer.module.css | 100 | rgba(16, 185, 129, 0.4) | Use --success-glow equivalent |
| FocusTimer.module.css | 155 | rgba(245, 158, 11, 0.3) | Use --warning-glow equivalent |
| PactCard.module.css | 155 | rgba(16, 185, 129, 0.3) | Use --success-glow |
| Sidebar.module.css | 216 | rgba(99, 102, 241, 0.3) | rgba(var(--accent-primary-rgb), 0.3) |
| NotificationBell.module.css | 91, 151 | rgba(99, 102, 241, 0.1/0.05) | rgba(var(--accent-primary-rgb), ...) |
| Toast.module.css | 55-96 | 8 hardcoded rgba() for status colors | Use --success/danger/warning/info-glow/bg vars |
| NudgeButton.module.css | 20 | rgba(245, 158, 11, 0.2) | var(--warning-glow) |
| CreateTaskModal.module.css | 175 | rgba(59, 130, 246, 0.15) | rgba(var(--accent-primary-rgb), 0.15) |
| StatsPage.module.css | 246 | rgba(139, 92, 246, 0.15) | rgba(var(--accent-primary-rgb), 0.15) |
| GroupDetail.module.css | 76, 91-93 | rgba(139, 92, 246, ...) | rgba(var(--accent-primary-rgb), ...) |
| page.module.css | 631-633 | rgba(99, 102, 241, ...) | rgba(var(--accent-primary-rgb), ...) |

## Missing Dark Mode Overrides (20 files)
Most impactful files needing [data-theme="dark"] overrides:
1. FocusTimer.module.css — timer ring glow barely visible in dark mode (confirmed by Notion visual audit)
2. PactCard.module.css — completed card text low contrast in dark (confirmed by Notion visual audit)
3. GroupDetail.module.css — hardcoded violet doesn't adapt to accent palette
4. Toast.module.css — all 4 status variants need dark adjustments
5. CompactActivityCard.module.css — heatmap cells, tooltip, hover shadows
6. DailySummaryCard.module.css — freeze badge, milestone/risk banners
7. ActivityFeed.module.css, ActivityItem.module.css, ActivityComments.module.css
8. NotificationBell.module.css, XPBar.module.css, CommandPalette.module.css
9. OnboardingChecklist.module.css, StreakHero.module.css, NudgeButton.module.css
10. page.module.css, ShareStreak.module.css, StatsPage.module.css, GroupStats.module.css

Note: Many components rely on globals.css variable swaps which handles most dark mode. Issues above are specific hardcoded rgba values or custom glows that don't adapt.

## Animation Violations (6 components)
| File | Issue | Correct Preset |
|------|-------|---------------|
| MobileNav.js:101 | Inline whileTap={{ scale: 0.95 }} | buttonTap |
| MobileNav.js:128 | Inline spring transition | shared spring config |
| Toast.js:51 | Inline initial/animate/transition | fadeInUp |
| NotificationBell.js:114-172 | Multiple inline animations | scaleIn, fadeInUp, buttonTap |
| Navbar.js:35-87 | Inline entrance animations | fadeInDown or pageTransition |
| CompactActivityCard.js:139-144 | Inline variants object | cardHover |

## Quick Visual Wins (<1hr each)
| File | CSS Class | Current | Recommended |
|------|-----------|---------|-------------|
| Sidebar.module.css:216 | .miniTimer | hardcoded rgba(99,102,241) | rgba(var(--accent-primary-rgb), 0.3) |
| NotificationBell.module.css:91,151 | .markAllBtn:hover | hardcoded indigo | rgba(var(--accent-primary-rgb), ...) |
| GroupDetail.module.css:76,91 | .lockedInBadge | hardcoded violet | rgba(var(--accent-primary-rgb), ...) |
| page.module.css:631 | .mockLevel1-3 | hardcoded indigo | rgba(var(--accent-primary-rgb), ...) |
| page.module.css | .heroTitle, .statNumber | hardcoded font-weight | var(--font-extrabold), var(--font-bold) |

## Component Redesigns (1-4hr each)
| Component | File | Issue | Recommended Approach |
|-----------|------|-------|---------------------|
| Toast | Toast.module.css + Toast.js | All hardcoded status rgba, no dark mode, no animation presets | Replace with CSS vars, add dark overrides, use fadeInUp preset |
| NotificationBell | NotificationBell.module.css + .js | Hardcoded accent (breaks non-indigo palettes), inline animations | Use --accent-primary-rgb, import presets |
| CompactActivityCard | CompactActivityCard.module.css + .js | Hardcoded font-sizes, gaps, inline variants, no dark mode | Convert to tokens, import cardHover, add dark mode |
| ActivityComments | ActivityComments.module.css | Hardcoded font sizes (9-13px), no dark mode | Normalize to --text-xs/--text-sm, add dark overrides |
| Landing Placeholder | page.module.css .screenshotPlaceholder | Gray box placeholder — weakest visual element | Replace with actual screenshot or remove entirely |

## Design System Recommendations
| Type | Name | Value | Reasoning |
|------|------|-------|-----------|
| Token | --accent-primary-rgb | R, G, B values of accent | Needed for rgba() with opacity — currently most components hardcode indigo RGB |
| Token | --success-rgb | 16, 185, 129 | Needed for FocusTimer, PactCard success glows with custom opacity |
| Token | --font-extrabold | 800 | Used on landing hero but hardcoded |
| Token | --font-bold | 700 | Used on stat numbers but hardcoded |

## Top 5 Highest Impact Changes
1. **Add --accent-primary-rgb token + replace hardcoded accent rgba() (~2hr)** — 6+ files locked to indigo/violet that break on other accent palettes
2. **Toast component CSS variable migration (~1hr)** — 8 hardcoded rgba() values, high-visibility component seen constantly
3. **Remove prefers-color-scheme media queries (~15min)** — DONE in this triage (CreatePactModal, Sidebar)
4. **Import animation presets in 6 components (~2hr)** — MobileNav, Toast, NotificationBell, Navbar, CompactActivityCard most impactful
5. **Hardcoded font-sizes to typography tokens (~2hr)** — 31 instances across 12 files, DailySummaryCard and ActivityComments most impactful
Loading
Loading