Skip to content

Dev - #24

Merged
Arun-kushwaha007 merged 14 commits into
mainfrom
dev
Apr 16, 2026
Merged

Dev#24
Arun-kushwaha007 merged 14 commits into
mainfrom
dev

Conversation

@Arun-kushwaha007

@Arun-kushwaha007 Arun-kushwaha007 commented Apr 11, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Add “Delete group” flow with confirmation modal, owner-only soft-delete, toast feedback, and return to dashboard.
    • Theme switcher: toggle Light / Dark / System.
  • Improvements

    • Soft-deleted groups are excluded from lists, summaries, and dashboards.
    • Unified theme/color tokens; dark is now the default root theme and card surface styling updated.
    • Dashboard summary derives primary currency and adjusts total balance label for multi-group views.
  • UI

    • Modals, portals, overlays, and various components updated for improved stacking, focus, and theme-aware styling.

Replace hardcoded colors and styles in dashboard components and layout with CSS custom properties for dynamic theming support. Introduce ThemeToggle component for user-controlled theme switching.
…faults

Replace remaining hardcoded color values in components and styles with CSS custom properties for full theming support. Switch default theme to dark mode, update light mode color palette for improved contrast and consistency, and refine theme resolution logic in ThemeProvider. This enhances maintainability and ensures uniform application of themes across layouts and UI elements.
…ming

Add Portal component for improved modal rendering, refactor CreateExpenseModal and CreateGroupModal to use portals with updated z-indexes and backgrounds. Introduce cardSolid color variable for solid backgrounds in themes. Update dashboard to dynamically use group currency and label multi-group balances.

BREAKING CHANGE: Modal z-indexes adjusted for layering consistency.
Introduce soft delete for groups in the database schema and backend service, allowing group owners to delete groups via a new API endpoint. Update frontend to include a delete group modal in group actions, with proper confirmation and action handling. Modify queries to exclude deleted groups from user summaries and dashboards.

BREAKING CHANGE: Group deletion now permanently removes group data from active views and balances.
@vercel

vercel Bot commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

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

Project Deployment Actions Updated (UTC)
fair-share-web Ready Ready Preview, Comment Apr 16, 2026 6:07pm

@coderabbitai

coderabbitai Bot commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds soft-delete support for groups (deletedAt), an authenticated owner-only DELETE /groups/:id that soft-deletes and invalidates caches, a frontend delete UI (modal + action), wide theme/styling refactors to CSS variables with ThemeToggle and Portal-based modals, and CI + Playwright E2E additions.

Changes

Cohort / File(s) Summary
Backend Soft-Delete & Cache
apps/backend/prisma/schema.prisma, apps/backend/src/groups/groups.controller.ts, apps/backend/src/groups/groups.service.ts, apps/backend/src/redis/redis.service.ts
Add Group.deletedAt: DateTime?. New DELETE /groups/:id route calls GroupsService.delete (owner-only), list/dashboard queries filter deletedAt: null, soft-delete sets deletedAt, disables sharing and clears token, and invalidates group and user dashboard caches.
Frontend Delete Flow
apps/web/src/components/groups/DeleteGroupModal.tsx, apps/web/src/components/groups/GroupActions.tsx, apps/web/src/lib/actions.ts, apps/web/app/dashboard/groups/[groupId]/page.tsx
Add DeleteGroupModal and wire it into GroupActions (adds groupName prop). New server action deleteGroupAction issues DELETE to backend; group detail page passes groupName to actions.
Portal & Modal Refactor
apps/web/src/components/ui/Portal.tsx, apps/web/src/components/groups/CreateExpenseModal.tsx, apps/web/src/components/groups/CreateGroupModal.tsx
Introduce Portal component and migrate modals into portal with adjusted z-index, backdrop behavior, layout padding, and updated form input styling (border-2, surface backgrounds, placeholders, hover states).
Theme System & Toggle
apps/web/src/components/theme/ThemeProvider.tsx, apps/web/src/components/theme/ThemeToggle.tsx, apps/web/app/layout.tsx, apps/web/app/theme.css, apps/web/src/design/theme.ts
Refactor theme resolution (compute-only resolved), remove runtime DOM theme init, switch root/default theme to dark with [data-theme='light'] override, add ThemeToggle, and expose --fs-card-solid theme var.
Design Tokens & Styling Migration
apps/web/src/design/colors.ts, apps/web/app/globals.css, apps/web/src/design/theme.ts, apps/web/src/components/ui/GlassCard.tsx, apps/web/src/components/dashboard/..., apps/web/src/components/profile/ProfilePanel.tsx, apps/web/src/components/layout/..., apps/web/components/home/GridBackground.tsx, apps/web/app/dashboard/page.tsx, apps/web/src/components/dashboard/SummaryCard.tsx
Expanded color palette (added cardSolid and new palette entries) and replaced many hardcoded Tailwind colors with --fs-* CSS variables across styles and components; minor UI text/label adjustments.
Client Layout & UI Additions
apps/web/src/components/layout/DashboardLayout.tsx, apps/web/src/components/layout/GuestLayout.tsx, apps/web/src/components/layout/Topbar.tsx, apps/web/src/components/layout/Sidebar.tsx
Mark DashboardLayout as client, add data-theme={resolved}, switch hardcoded backgrounds to theme vars with transitions, insert ThemeToggle in Topbar, and adjust card backgrounds/shadows.
Infra, Tests & CI
.github/workflows/ci.yml, playwright.config.ts, tests/e2e/dashboard.spec.ts, test-results/.last-run.json, .gitignore, turbo.json
Reorder CI to run prisma push and E2E (start DB services, install Playwright deps, run E2E); split Playwright projects (api/web) with conditional webServer in CI; add dashboard E2E test and test-results artifact; ignore test-results; update turbo lint dependency.
Test Fixes & Mocks
apps/backend/src/receipts/receipts.service.spec.ts, apps/backend/src/settlements/settlements.integration.spec.ts
Adjust test constructor argument ordering and Prisma mock expectations (e.g., findUnique with select) to match implementation changes.
Various UI Styling Updates
apps/web/src/components/dashboard/ActivityList.tsx, apps/web/src/components/dashboard/SpendingChart.tsx, apps/web/src/components/dashboard/SummaryCard.tsx, apps/web/src/components/groups/CreateExpenseModal.tsx, apps/web/src/components/groups/CreateGroupModal.tsx, apps/web/src/components/profile/ProfilePanel.tsx
Refactor components to use theme CSS variables for borders/backgrounds/text, add subtle shadows and hover behaviors; no control-flow changes.

Sequence Diagram

sequenceDiagram
    actor User
    participant Client as Web Client
    participant Modal as DeleteGroupModal
    participant Action as deleteGroupAction
    participant API as Backend API
    participant Service as GroupsService
    participant DB as Database
    participant Redis as RedisService

    User->>Client: Click "Delete group"
    Client->>Modal: Open DeleteGroupModal
    User->>Modal: Confirm deletion
    Modal->>Modal: setIsDeleting(true)
    Modal->>Action: deleteGroupAction(groupId)
    Action->>API: DELETE /groups/:id (Auth)
    API->>Service: groupsService.delete(groupId, userId)
    Service->>Service: Verify user is OWNER
    Service->>DB: Update group (set deletedAt, shareEnabled=false, clear token)
    DB-->>Service: Success
    Service->>Redis: invalidateGroupCache(groupId)
    Service->>Redis: invalidateUserDashboardCache(userId)
    Service-->>API: { success: true }
    API-->>Action: 200 OK
    Action-->>Modal: { success: true }
    Modal->>Client: Show toast, close modal, navigate /dashboard, refresh
    Client->>User: Display updated dashboard (soft-deleted group excluded)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Poem

🐰✨ I hopped through vars and colors bright,
Soft-deleted groups tucked out of sight,
Portals popped where modals play,
Dark and light now swap by day,
Caches cleared — all snug tonight!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Dev' is vague and generic; it does not convey any meaningful information about the substantive changes in the changeset. Replace the title with a clear, specific description of the main change. For example: 'Add group soft-delete functionality and theme system updates' or break into multiple PRs with focused titles.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/backend/src/groups/groups.service.ts (1)

253-270: ⚠️ Potential issue | 🔴 Critical

Restore the missing findMany call in getDashboard().

This block no longer parses: where, include, and orderBy are floating inside the function body, and groups is never declared. The current PR breaks backend compilation.

Minimal fix
   async getDashboard(userId: string): Promise<GroupDashboardDto> {
+    const groups = await this.prisma.group.findMany({
       where: {
         members: {
           some: {
             userId,
           },
         },
         deletedAt: null,
       },
       include: {
         _count: {
           select: {
             members: true,
           },
         },
       },
       orderBy: { createdAt: 'desc' },
     });
 
     const groupIds = groups.map((group) => group.id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/groups/groups.service.ts` around lines 253 - 270, The
function getDashboard has stray query option objects and never calls the ORM;
wrap the existing where/include/orderBy into an awaited Prisma findMany call and
assign its result to a groups variable. Specifically, replace the floating
blocks with: const groups = await this.prisma.group.findMany({ where: { members:
{ some: { userId } }, deletedAt: null }, include: { _count: { select: { members:
true } } }, orderBy: { createdAt: 'desc' } }); so subsequent logic in
getDashboard() can use the populated groups array.
apps/web/src/components/groups/GroupActions.tsx (1)

194-223: ⚠️ Potential issue | 🟠 Major

Hide the delete flow for non-owners.

The backend only allows OWNER users to delete a group, but this button and modal are rendered for everyone. Every regular member will hit a guaranteed forbidden response here.

Suggested direction
 type GroupActionsProps = {
   groupId: string;
   groupName: string;
+  canDelete: boolean;
   currency: CurrencyCode;
   members: GroupMemberSummaryDto[];
   shareEnabled: boolean;
   shareToken?: string | null;
   defaultSplitPreference?: GroupDefaultSplitDto | null;
 };
 
 export function GroupActions({
   groupId,
   groupName,
+  canDelete,
   currency,
   members,
   shareEnabled,
   shareToken,
   defaultSplitPreference,
 }: GroupActionsProps) {
@@
-          <button
-            type="button"
-            onClick={() => setDeleteOpen(true)}
-            className="flex items-center justify-center gap-2 rounded-xl border border-rose-500/20 bg-rose-500/5 px-4 py-3 text-sm font-bold text-rose-600 hover:bg-rose-500/10 transition-colors"
-          >
-            <Trash2 className="h-4 w-4" />
-            Delete group
-          </button>
+          {canDelete && (
+            <button
+              type="button"
+              onClick={() => setDeleteOpen(true)}
+              className="flex items-center justify-center gap-2 rounded-xl border border-rose-500/20 bg-rose-500/5 px-4 py-3 text-sm font-bold text-rose-600 hover:bg-rose-500/10 transition-colors"
+            >
+              <Trash2 className="h-4 w-4" />
+              Delete group
+            </button>
+          )}
@@
-      <DeleteGroupModal
-        groupId={groupId}
-        groupName={groupName}
-        open={deleteOpen}
-        onClose={() => setDeleteOpen(false)}
-      />
+      {canDelete && (
+        <DeleteGroupModal
+          groupId={groupId}
+          groupName={groupName}
+          open={deleteOpen}
+          onClose={() => setDeleteOpen(false)}
+        />
+      )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/groups/GroupActions.tsx` around lines 194 - 223, In
GroupActions, hide the delete flow for non-owners by conditionally rendering the
Delete button and the DeleteGroupModal only when the current user is the owner;
use an ownership check (e.g., a boolean prop isOwner or compare currentUserId to
the group's ownerId) and wrap the JSX that references setDeleteOpen, deleteOpen,
and <DeleteGroupModal groupId={groupId} groupName={groupName} ... /> in that
guard so regular members never see or trigger the forbidden delete flow.
apps/web/src/components/theme/ThemeProvider.tsx (1)

23-60: ⚠️ Potential issue | 🟠 Major

Apply the resolved theme back to the document root.

resolved is now only stored in React state. Since the stylesheet switches on [data-theme="light"], light mode will not reach the app shell unless some nested layout manually adds that attribute. Navbar/Footer in apps/web/app/layout.tsx will stay on the dark :root vars.

Minimal fix
 export function ThemeProvider({ children }: { children: React.ReactNode }) {
   const [mode, setModeState] = useState<ThemeMode>('system');
   const [resolved, setResolved] = useState<ResolvedTheme>('dark');
@@
   useEffect(() => {
     const media = window.matchMedia('(prefers-color-scheme: dark)');
@@
   }, [mode]);
+
+  useEffect(() => {
+    document.documentElement.dataset.theme = resolved;
+    document.body.dataset.theme = resolved;
+  }, [resolved]);
 
   const setMode = (next: ThemeMode) => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/theme/ThemeProvider.tsx` around lines 23 - 60, The
component updates only React state but never applies the theme to the DOM; add a
sync that writes the resolved theme to the document root so CSS selectors like
[data-theme="light"] take effect. Implement a useEffect in ThemeProvider that
depends on resolved and sets document.documentElement.setAttribute('data-theme',
resolved) (or removes the attribute if you prefer treating 'dark' as default)
and ensure this runs after your initial readStored/setResolved and after
setResolved(resolveTheme(next)) in setMode so the document root always reflects
the current resolved theme; reference resolveTheme, ThemeProvider, resolved,
setResolved, setMode and STORAGE_KEY to locate the places to update.
🧹 Nitpick comments (2)
apps/web/src/lib/actions.ts (1)

377-383: Encode groupId before interpolating it into the request path.

This makes the action resilient to unexpected/special characters in identifiers.

🔧 Suggested fix
-  const response = await fetch(`${getBackendBaseUrl()}/groups/${groupId}`, {
+  const response = await fetch(`${getBackendBaseUrl()}/groups/${encodeURIComponent(groupId)}`, {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/lib/actions.ts` around lines 377 - 383, The DELETE request URL
currently interpolates groupId directly into the path; update the call that
constructs the fetch URL (the expression using getBackendBaseUrl() and groupId
in apps/web/src/lib/actions.ts) to encode the identifier with
encodeURIComponent(groupId) before interpolation so special characters are
safely escaped; keep the rest of the fetch options (method, headers, cache)
unchanged.
apps/web/src/components/groups/CreateExpenseModal.tsx (1)

273-277: Consider unifying modal z-index tokens across dialogs.

These hardcoded layers (z-[80] / z-[110]) differ from other modals (apps/web/src/components/groups/CreateGroupModal.tsx and apps/web/src/components/groups/DeleteGroupModal.tsx), which can create inconsistent overlay ordering if dialogs stack.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/groups/CreateExpenseModal.tsx` around lines 273 -
277, The hardcoded z-index classes in CreateExpenseModal (the backdrop div with
z-[80] and the wrapper div with z-[110]) should be unified with the shared modal
z-index tokens used by CreateGroupModal and DeleteGroupModal; replace the
literal z-[80]/z-[110] with the common token classes/variables (or import the
shared constants from your UI/token module) for backdrop and content layers so
all dialogs use the same stacking order and avoid inconsistent overlay ordering
when dialogs stack.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/backend/prisma/schema.prisma`:
- Line 70: Add a soft-delete guard to every group read so deleted groups are
never returned or modified: update the group lookup calls in getGroupById,
toggleShare, getGroupByShareToken and the balance export logic
(balances.service.ts) to include a Prisma where clause filtering on deletedAt:
null (or the equivalent query guard used across the codebase) and ensure any
single-group fetches use this same condition before returning or performing
mutations; if a lookup already combines other where filters, merge deletedAt:
null into that object so all reads and any share/token lookups reject
soft-deleted groups.

In `@apps/backend/src/groups/groups.service.ts`:
- Around line 677-698: The soft-delete only sets deletedAt but other methods
(e.g. assertMembership(), any findUnique/findFirst/getById/group lookup helpers)
still return groups with deletedAt set; update those lookup functions and any
direct group reads/updates in this file to filter out soft-deleted groups by
adding a condition like where: { id: groupId, deletedAt: null } (or include
deletedAt: null in composite where clauses used by assertMembership()), and make
them throw NotFound/Forbidden when the group is not found; ensure all usages of
assertMembership(), findUnique/findFirst on Group, and any group update/read
helpers reference this deletedAt check so deleted groups are inaccessible via
member endpoints.

In `@apps/web/app/dashboard/page.tsx`:
- Around line 62-71: The totalBalanceCents is being formatted with
primaryCurrency (groups[0]?.currency) which incorrectly presents a
mixed-currency aggregate as a single currency; compute the set of distinct
currencies from groups (e.g., map groups -> currency and dedupe) and if more
than one currency exists, do not call formatMoney(totalBalanceCents,
primaryCurrency); instead set a clear fallback value (e.g., "Multiple
currencies" or render a per-currency breakdown) and pass that to
SummaryCard.value; when all groups share the same currency continue using
primaryCurrency with formatMoney. Ensure you update the block around
primaryCurrency, totalBalanceCents, SummaryCard and any display logic to use an
isMixedCurrencies guard (distinct currencies check) before formatting.

In `@apps/web/src/components/groups/CreateExpenseModal.tsx`:
- Around line 286-287: The modal container in CreateExpenseModal.tsx currently
uses conflicting overflow utilities ("overflow-y-auto" and "overflow-hidden")
that can prevent vertical scrolling; edit the element that defines className
(the modal wrapper) to remove "overflow-hidden" and keep "overflow-y-auto"
(ensure the existing "max-h-[90vh]" remains) so the form can scroll on small
screens without being clipped.

In `@apps/web/src/components/groups/CreateGroupModal.tsx`:
- Around line 76-80: The close button in CreateGroupModal (the button using
onClick={onClose}) is icon-only and lacks accessible semantics; add
type="button" and an appropriate aria-label (e.g., aria-label="Close" or
aria-label="Close dialog") to that button element so screen readers can announce
its purpose while preserving the existing className and onClick handler.

In `@apps/web/src/components/groups/DeleteGroupModal.tsx`:
- Around line 26-43: The handleDelete function currently only calls
setIsDeleting(false) in the catch path, leaving isDeleting true on success; move
the teardown into a finally block so setIsDeleting(false) always runs.
Specifically, keep setIsDeleting(true) at the start of handleDelete, keep the
deleteGroupAction result checks and success flow (toast, onClose, router.push,
router.refresh), and add a finally that calls setIsDeleting(false) (and
optionally ensure setError(null) stays where it belongs) so state is reset
regardless of success or failure.
- Around line 51-76: The modal root (motion.div ref={modalRef}) lacks an
accessible name and the icon-only close button (onClick={onClose}) lacks an
aria-label; add aria-labelledby on the dialog pointing to the heading's id (give
the h3 a unique id like delete-group-title) so role="dialog" is programmatically
named, and add an appropriate aria-label (e.g., "Close" or "Close delete
dialog") to the close button (the element rendering <X />) to make it
screen-reader accessible; update DeleteGroupModal's JSX accordingly.

In `@apps/web/src/components/theme/ThemeToggle.tsx`:
- Around line 12-44: The three icon-only buttons that call
setMode('light'|'dark'|'system') should be made accessible: add type="button",
an explicit aria-label (e.g., "Light mode", "Dark mode", "System preference")
and an aria-pressed attribute that reflects the current mode (aria-pressed={mode
=== 'light' | 'dark' | 'system'} respectively). Update the buttons that render
Sun, Moon and Laptop so their aria-labels and aria-pressed state match the value
passed to setMode to ensure screen readers see the name and active state.

In `@apps/web/src/components/ui/Portal.tsx`:
- Line 16: The Portal currently uses createPortal(children, document.body) which
breaks theme scoping because it mounts outside the element that holds
data-theme; update the Portal component to locate the nearest theme-scoped
container (e.g., the closest ancestor or document.querySelector('[data-theme]'))
and mount the portal into that element (or create and append a dedicated wrapper
div inside that container) instead of document.body so modal/content inherits
the correct light-theme CSS variables; update references in the Portal code
where createPortal is called to use the found themeContainer.

---

Outside diff comments:
In `@apps/backend/src/groups/groups.service.ts`:
- Around line 253-270: The function getDashboard has stray query option objects
and never calls the ORM; wrap the existing where/include/orderBy into an awaited
Prisma findMany call and assign its result to a groups variable. Specifically,
replace the floating blocks with: const groups = await
this.prisma.group.findMany({ where: { members: { some: { userId } }, deletedAt:
null }, include: { _count: { select: { members: true } } }, orderBy: {
createdAt: 'desc' } }); so subsequent logic in getDashboard() can use the
populated groups array.

In `@apps/web/src/components/groups/GroupActions.tsx`:
- Around line 194-223: In GroupActions, hide the delete flow for non-owners by
conditionally rendering the Delete button and the DeleteGroupModal only when the
current user is the owner; use an ownership check (e.g., a boolean prop isOwner
or compare currentUserId to the group's ownerId) and wrap the JSX that
references setDeleteOpen, deleteOpen, and <DeleteGroupModal groupId={groupId}
groupName={groupName} ... /> in that guard so regular members never see or
trigger the forbidden delete flow.

In `@apps/web/src/components/theme/ThemeProvider.tsx`:
- Around line 23-60: The component updates only React state but never applies
the theme to the DOM; add a sync that writes the resolved theme to the document
root so CSS selectors like [data-theme="light"] take effect. Implement a
useEffect in ThemeProvider that depends on resolved and sets
document.documentElement.setAttribute('data-theme', resolved) (or removes the
attribute if you prefer treating 'dark' as default) and ensure this runs after
your initial readStored/setResolved and after setResolved(resolveTheme(next)) in
setMode so the document root always reflects the current resolved theme;
reference resolveTheme, ThemeProvider, resolved, setResolved, setMode and
STORAGE_KEY to locate the places to update.

---

Nitpick comments:
In `@apps/web/src/components/groups/CreateExpenseModal.tsx`:
- Around line 273-277: The hardcoded z-index classes in CreateExpenseModal (the
backdrop div with z-[80] and the wrapper div with z-[110]) should be unified
with the shared modal z-index tokens used by CreateGroupModal and
DeleteGroupModal; replace the literal z-[80]/z-[110] with the common token
classes/variables (or import the shared constants from your UI/token module) for
backdrop and content layers so all dialogs use the same stacking order and avoid
inconsistent overlay ordering when dialogs stack.

In `@apps/web/src/lib/actions.ts`:
- Around line 377-383: The DELETE request URL currently interpolates groupId
directly into the path; update the call that constructs the fetch URL (the
expression using getBackendBaseUrl() and groupId in apps/web/src/lib/actions.ts)
to encode the identifier with encodeURIComponent(groupId) before interpolation
so special characters are safely escaped; keep the rest of the fetch options
(method, headers, cache) unchanged.
🪄 Autofix (Beta)

✅ Autofix completed


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bc221e6c-c53f-4be8-9500-dbe50bc31b25

📥 Commits

Reviewing files that changed from the base of the PR and between 3766ba5 and 73b5fea.

📒 Files selected for processing (28)
  • apps/backend/prisma/schema.prisma
  • apps/backend/src/groups/groups.controller.ts
  • apps/backend/src/groups/groups.service.ts
  • apps/web/app/dashboard/groups/[groupId]/page.tsx
  • apps/web/app/dashboard/page.tsx
  • apps/web/app/globals.css
  • apps/web/app/layout.tsx
  • apps/web/app/theme.css
  • apps/web/components/home/GridBackground.tsx
  • apps/web/components/ui/GlassCard.tsx
  • apps/web/src/components/dashboard/ActivityList.tsx
  • apps/web/src/components/dashboard/SpendingChart.tsx
  • apps/web/src/components/dashboard/SummaryCard.tsx
  • apps/web/src/components/groups/CreateExpenseModal.tsx
  • apps/web/src/components/groups/CreateGroupModal.tsx
  • apps/web/src/components/groups/DeleteGroupModal.tsx
  • apps/web/src/components/groups/GroupActions.tsx
  • apps/web/src/components/layout/DashboardLayout.tsx
  • apps/web/src/components/layout/GuestLayout.tsx
  • apps/web/src/components/layout/Sidebar.tsx
  • apps/web/src/components/layout/Topbar.tsx
  • apps/web/src/components/profile/ProfilePanel.tsx
  • apps/web/src/components/theme/ThemeProvider.tsx
  • apps/web/src/components/theme/ThemeToggle.tsx
  • apps/web/src/components/ui/Portal.tsx
  • apps/web/src/design/colors.ts
  • apps/web/src/design/theme.ts
  • apps/web/src/lib/actions.ts

shareToken String? @unique
shareEnabled Boolean @default(false)
createdAt DateTime @default(now())
deletedAt DateTime?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Group queries:"
rg -n --type=ts -C2 'prisma\.group\.(findUnique|findFirst|findMany)\(' apps/backend/src

echo
echo "2) Soft-delete filters present:"
rg -n --type=ts -C2 'deletedAt\s*:\s*null' apps/backend/src/groups apps/backend/src/balances

Repository: Arun-kushwaha007/FairShare

Length of output: 3109


🏁 Script executed:

#!/bin/bash

echo "=== Checking groups.service.ts around line 114 ==="
sed -n '110,130p' apps/backend/src/groups/groups.service.ts

echo
echo "=== Checking groups.service.ts around line 380 ==="
sed -n '375,390p' apps/backend/src/groups/groups.service.ts

echo
echo "=== Checking groups.service.ts around line 418 ==="
sed -n '415,435p' apps/backend/src/groups/groups.service.ts

echo
echo "=== Checking balances.service.ts around line 50 ==="
sed -n '45,65p' apps/backend/src/balances/balances.service.ts

Repository: Arun-kushwaha007/FairShare

Length of output: 2835


Enforce soft-delete filters on all user-facing group read operations.

Current group queries bypass soft-delete checks in several critical paths:

  • getGroupById() (line 114): No deletedAt filter; returns deleted groups
  • toggleShare() (line 380): No deletedAt filter on lookup; allows modifications to deleted groups
  • getGroupByShareToken() (line 418): No deletedAt filter; public share links to deleted groups remain accessible
  • Balance export in balances.service.ts (line 50): No deletedAt filter; deleted group data can be exported

All group reads must include where: { ..., deletedAt: null } or equivalent guard to prevent access to deleted groups.

🧰 Tools
🪛 GitHub Actions: CI

[error] Command failed with exit code 1: tsc --noEmit (backend@1.0.0 lint).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/prisma/schema.prisma` at line 70, Add a soft-delete guard to
every group read so deleted groups are never returned or modified: update the
group lookup calls in getGroupById, toggleShare, getGroupByShareToken and the
balance export logic (balances.service.ts) to include a Prisma where clause
filtering on deletedAt: null (or the equivalent query guard used across the
codebase) and ensure any single-group fetches use this same condition before
returning or performing mutations; if a lookup already combines other where
filters, merge deletedAt: null into that object so all reads and any share/token
lookups reject soft-deleted groups.

Comment on lines +677 to +698
async delete(groupId: string, actorUserId: string): Promise<{ success: true }> {
const membership = await this.prisma.groupMember.findUnique({
where: {
groupId_userId: {
groupId,
userId: actorUserId,
},
},
});

if (!membership || membership.role !== 'OWNER') {
throw new ForbiddenException('Only the group owner can delete the group');
}

await this.prisma.group.update({
where: { id: groupId },
data: {
deletedAt: new Date(),
shareEnabled: false,
shareToken: null
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Soft-deleted groups are still accessible through existing member endpoints.

This only hides deleted groups from list/dashboard-style queries. Because assertMembership() and the other groupId lookups do not check deletedAt, a member with an old URL can still fetch or mutate a deleted group after this method runs.

Suggested direction
-  private async assertMembership(groupId: string, userId: string): Promise<void> {
-    const membership = await this.prisma.groupMember.findUnique({
-      where: {
-        groupId_userId: {
-          groupId,
-          userId,
-        },
-      },
-    });
+  private async assertMembership(groupId: string, userId: string): Promise<void> {
+    const membership = await this.prisma.groupMember.findFirst({
+      where: {
+        groupId,
+        userId,
+        group: {
+          deletedAt: null,
+        },
+      },
+    });
 
     if (!membership) {
       throw new ForbiddenException('Actor is not a group member');
     }
   }

The same rule needs to be applied to direct group reads/updates in this file, otherwise deleted groups remain operational through stale clients.

🧰 Tools
🪛 Biome (2.4.10)

[error] 677-677: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 677-677: expected ) but instead found :

(parse)


[error] 677-677: the target for a delete operator cannot be a single identifier

(parse)


[error] 677-677: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 677-677: Expected a statement but instead found '>'.

(parse)

🪛 GitHub Actions: CI

[error] Command failed with exit code 1: tsc --noEmit (backend@1.0.0 lint).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/groups/groups.service.ts` around lines 677 - 698, The
soft-delete only sets deletedAt but other methods (e.g. assertMembership(), any
findUnique/findFirst/getById/group lookup helpers) still return groups with
deletedAt set; update those lookup functions and any direct group reads/updates
in this file to filter out soft-deleted groups by adding a condition like where:
{ id: groupId, deletedAt: null } (or include deletedAt: null in composite where
clauses used by assertMembership()), and make them throw NotFound/Forbidden when
the group is not found; ensure all usages of assertMembership(),
findUnique/findFirst on Group, and any group update/read helpers reference this
deletedAt check so deleted groups are inaccessible via member endpoints.

Comment on lines +62 to +71
const primaryCurrency = groups[0]?.currency ?? 'USD';
const totalBalanceLabel = groups.length > 1 ? 'Multi-Crew Balance' : 'Total Balance';

return (
<DashboardLayout>
<div className="space-y-8">
<div className="grid grid-cols-2 gap-3 sm:gap-4 md:grid-cols-4">
<SummaryCard
title="Total Balance"
value={formatMoney(totalBalanceCents, 'USD')}
title={totalBalanceLabel}
value={formatMoney(totalBalanceCents, primaryCurrency)}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't render a mixed-currency sum as one currency.

dashboard.totalBalanceCents is aggregated across all groups, but this now formats it using groups[0]?.currency. If a user has both USD and EUR groups, the card will show a mixed total as if it were entirely USD/EUR.

Suggested guard
   const primaryCurrency = groups[0]?.currency ?? 'USD';
+  const hasSingleCurrency = new Set(groups.map((group) => group.currency)).size <= 1;
   const totalBalanceLabel = groups.length > 1 ? 'Multi-Crew Balance' : 'Total Balance';
@@
           <SummaryCard
             title={totalBalanceLabel}
-            value={formatMoney(totalBalanceCents, primaryCurrency)}
+            value={hasSingleCurrency ? formatMoney(totalBalanceCents, primaryCurrency) : '—'}
             icon="dollar"
             change="Live"
             trend={isPositive ? 'up' : 'down'}
-            hint={isPositive ? 'Surplus Protocol' : 'Deficit Detected'}
+            hint={
+              hasSingleCurrency
+                ? isPositive
+                  ? 'Surplus Protocol'
+                  : 'Deficit Detected'
+                : 'Multiple currencies'
+            }
           />

Longer term, this probably wants a per-currency breakdown from the backend instead of a single aggregate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/dashboard/page.tsx` around lines 62 - 71, The totalBalanceCents
is being formatted with primaryCurrency (groups[0]?.currency) which incorrectly
presents a mixed-currency aggregate as a single currency; compute the set of
distinct currencies from groups (e.g., map groups -> currency and dedupe) and if
more than one currency exists, do not call formatMoney(totalBalanceCents,
primaryCurrency); instead set a clear fallback value (e.g., "Multiple
currencies" or render a per-currency breakdown) and pass that to
SummaryCard.value; when all groups share the same currency continue using
primaryCurrency with formatMoney. Ensure you update the block around
primaryCurrency, totalBalanceCents, SummaryCard and any display logic to use an
isMixedCurrencies guard (distinct currencies check) before formatting.

Comment on lines +286 to +287
className="w-full max-h-[90vh] overflow-y-auto rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card-solid)] shadow-[var(--fs-shadow-elevated)] overflow-hidden"
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Conflicting overflow utilities can block modal scrolling.

Line 286 includes both overflow-y-auto and overflow-hidden; the latter can suppress vertical scroll and hide form content on smaller screens.

Suggested fix
-            className="w-full max-h-[90vh] overflow-y-auto rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card-solid)] shadow-[var(--fs-shadow-elevated)] overflow-hidden"
+            className="w-full max-h-[90vh] overflow-y-auto overflow-x-hidden rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card-solid)] shadow-[var(--fs-shadow-elevated)]"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
className="w-full max-h-[90vh] overflow-y-auto rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card-solid)] shadow-[var(--fs-shadow-elevated)] overflow-hidden"
>
className="w-full max-h-[90vh] overflow-y-auto overflow-x-hidden rounded-3xl border border-[var(--fs-border)] bg-[var(--fs-card-solid)] shadow-[var(--fs-shadow-elevated)]"
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/groups/CreateExpenseModal.tsx` around lines 286 -
287, The modal container in CreateExpenseModal.tsx currently uses conflicting
overflow utilities ("overflow-y-auto" and "overflow-hidden") that can prevent
vertical scrolling; edit the element that defines className (the modal wrapper)
to remove "overflow-hidden" and keep "overflow-y-auto" (ensure the existing
"max-h-[90vh]" remains) so the form can scroll on small screens without being
clipped.

Comment on lines +76 to 80
<button
onClick={onClose}
className="p-2 rounded-xl bg-[var(--fs-background)] hover:bg-[var(--fs-background)]/70 transition-colors text-[var(--fs-text-muted)]"
title="Close"
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add an accessible name to the icon-only close button.

Line 76 should include aria-label (and type="button" for explicit semantics).

Suggested fix
-            <button
+            <button
+              type="button"
               onClick={onClose}
+              aria-label="Close create group dialog"
               className="p-2 rounded-xl bg-[var(--fs-background)] hover:bg-[var(--fs-background)]/70 transition-colors text-[var(--fs-text-muted)]"
               title="Close"
             >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={onClose}
className="p-2 rounded-xl bg-[var(--fs-background)] hover:bg-[var(--fs-background)]/70 transition-colors text-[var(--fs-text-muted)]"
title="Close"
>
<button
type="button"
onClick={onClose}
aria-label="Close create group dialog"
className="p-2 rounded-xl bg-[var(--fs-background)] hover:bg-[var(--fs-background)]/70 transition-colors text-[var(--fs-text-muted)]"
title="Close"
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/groups/CreateGroupModal.tsx` around lines 76 - 80,
The close button in CreateGroupModal (the button using onClick={onClose}) is
icon-only and lacks accessible semantics; add type="button" and an appropriate
aria-label (e.g., aria-label="Close" or aria-label="Close dialog") to that
button element so screen readers can announce its purpose while preserving the
existing className and onClick handler.

Comment on lines +26 to +43
const handleDelete = async () => {
try {
setIsDeleting(true);
setError(null);
const result = await deleteGroupAction(groupId);

if (!result.success) {
throw new Error(result.message);
}

toast(`Group "${groupName}" deleted successfully`);
onClose();
router.push('/dashboard');
router.refresh();
} catch (err) {
setError((err as Error).message || 'Failed to delete group');
setIsDeleting(false);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reset isDeleting in a finally block.

Line 42 resets only in catch; success path leaves stale state until unmount. Use finally to guarantee cleanup.

Suggested fix
   const handleDelete = async () => {
     try {
       setIsDeleting(true);
       setError(null);
       const result = await deleteGroupAction(groupId);
       
       if (!result.success) {
         throw new Error(result.message);
       }

       toast(`Group "${groupName}" deleted successfully`);
       onClose();
       router.push('/dashboard');
       router.refresh();
     } catch (err) {
       setError((err as Error).message || 'Failed to delete group');
-      setIsDeleting(false);
+    } finally {
+      setIsDeleting(false);
     }
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/groups/DeleteGroupModal.tsx` around lines 26 - 43,
The handleDelete function currently only calls setIsDeleting(false) in the catch
path, leaving isDeleting true on success; move the teardown into a finally block
so setIsDeleting(false) always runs. Specifically, keep setIsDeleting(true) at
the start of handleDelete, keep the deleteGroupAction result checks and success
flow (toast, onClose, router.push, router.refresh), and add a finally that calls
setIsDeleting(false) (and optionally ensure setError(null) stays where it
belongs) so state is reset regardless of success or failure.

Comment on lines +51 to +76
<motion.div
ref={modalRef}
tabIndex={-1}
role="dialog"
aria-modal="true"
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="relative w-full max-w-md rounded-3xl border-2 border-rose-500/20 bg-[var(--fs-card-solid)] p-6 sm:p-8 shadow-[0_20px_50px_rgba(244,63,94,0.15)] overflow-hidden"
>
<div className="flex items-center justify-between mb-6">
<div className="flex h-12 w-12 items-center justify-center rounded-2xl bg-rose-500/10 text-rose-500">
<AlertTriangle className="h-6 w-6" />
</div>
<button
onClick={onClose}
className="p-2 rounded-xl bg-[var(--fs-background)] hover:bg-[var(--fs-background)]/70 transition-colors text-[var(--fs-text-muted)]"
>
<X className="h-5 w-5" />
</button>
</div>

<div className="space-y-4 text-center sm:text-left">
<h3 className="text-2xl font-extrabold tracking-tight text-[var(--fs-text-primary)]">
Delete Group?
</h3>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Dialog is missing an accessible name.

The dialog has role="dialog" but no aria-labelledby/aria-label. Also, the icon-only close button should have aria-label.

Suggested fix
             <motion.div
               ref={modalRef}
               tabIndex={-1}
               role="dialog"
               aria-modal="true"
+              aria-labelledby="delete-group-title"
               initial={{ opacity: 0, scale: 0.95, y: 20 }}
               animate={{ opacity: 1, scale: 1, y: 0 }}
               exit={{ opacity: 0, scale: 0.95, y: 20 }}
               className="relative w-full max-w-md rounded-3xl border-2 border-rose-500/20 bg-[var(--fs-card-solid)] p-6 sm:p-8 shadow-[0_20px_50px_rgba(244,63,94,0.15)] overflow-hidden"
             >
...
-                <button
+                <button
+                  type="button"
                   onClick={onClose}
+                  aria-label="Close delete group dialog"
                   className="p-2 rounded-xl bg-[var(--fs-background)] hover:bg-[var(--fs-background)]/70 transition-colors text-[var(--fs-text-muted)]"
                 >
...
-                <h3 className="text-2xl font-extrabold tracking-tight text-[var(--fs-text-primary)]">
+                <h3 id="delete-group-title" className="text-2xl font-extrabold tracking-tight text-[var(--fs-text-primary)]">
                   Delete Group?
                 </h3>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/groups/DeleteGroupModal.tsx` around lines 51 - 76,
The modal root (motion.div ref={modalRef}) lacks an accessible name and the
icon-only close button (onClick={onClose}) lacks an aria-label; add
aria-labelledby on the dialog pointing to the heading's id (give the h3 a unique
id like delete-group-title) so role="dialog" is programmatically named, and add
an appropriate aria-label (e.g., "Close" or "Close delete dialog") to the close
button (the element rendering <X />) to make it screen-reader accessible; update
DeleteGroupModal's JSX accordingly.

Comment on lines +12 to +44
<button
onClick={() => setMode('light')}
className={`flex h-8 w-8 items-center justify-center rounded-xl transition-all ${
mode === 'light'
? 'bg-[var(--fs-primary)] text-white shadow-md'
: 'text-[var(--fs-text-muted)] hover:bg-[var(--fs-primary)]/10 hover:text-[var(--fs-primary)]'
}`}
title="Light Mode"
>
<Sun size={16} />
</button>
<button
onClick={() => setMode('dark')}
className={`flex h-8 w-8 items-center justify-center rounded-xl transition-all ${
mode === 'dark'
? 'bg-[var(--fs-primary)] text-white shadow-md'
: 'text-[var(--fs-text-muted)] hover:bg-[var(--fs-primary)]/10 hover:text-[var(--fs-primary)]'
}`}
title="Dark Mode"
>
<Moon size={16} />
</button>
<button
onClick={() => setMode('system')}
className={`flex h-8 w-8 items-center justify-center rounded-xl transition-all ${
mode === 'system'
? 'bg-[var(--fs-primary)] text-white shadow-md'
: 'text-[var(--fs-text-muted)] hover:bg-[var(--fs-primary)]/10 hover:text-[var(--fs-primary)]'
}`}
title="System Preference"
>
<Laptop size={16} />
</button>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add accessible labels/state to icon-only toggle buttons.

These controls rely on title, but they should expose explicit button name/state for assistive tech (aria-label + aria-pressed). Also set type="button" for safety in form contexts.

♿ Suggested fix
       <button
+        type="button"
         onClick={() => setMode('light')}
+        aria-label="Light mode"
+        aria-pressed={mode === 'light'}
         className={`flex h-8 w-8 items-center justify-center rounded-xl transition-all ${
           mode === 'light'
             ? 'bg-[var(--fs-primary)] text-white shadow-md'
             : 'text-[var(--fs-text-muted)] hover:bg-[var(--fs-primary)]/10 hover:text-[var(--fs-primary)]'
         }`}
         title="Light Mode"
       >
         <Sun size={16} />
       </button>
       <button
+        type="button"
         onClick={() => setMode('dark')}
+        aria-label="Dark mode"
+        aria-pressed={mode === 'dark'}
         className={`flex h-8 w-8 items-center justify-center rounded-xl transition-all ${
           mode === 'dark'
             ? 'bg-[var(--fs-primary)] text-white shadow-md'
             : 'text-[var(--fs-text-muted)] hover:bg-[var(--fs-primary)]/10 hover:text-[var(--fs-primary)]'
         }`}
         title="Dark Mode"
       >
         <Moon size={16} />
       </button>
       <button
+        type="button"
         onClick={() => setMode('system')}
+        aria-label="System theme"
+        aria-pressed={mode === 'system'}
         className={`flex h-8 w-8 items-center justify-center rounded-xl transition-all ${
           mode === 'system'
             ? 'bg-[var(--fs-primary)] text-white shadow-md'
             : 'text-[var(--fs-text-muted)] hover:bg-[var(--fs-primary)]/10 hover:text-[var(--fs-primary)]'
         }`}
         title="System Preference"
       >
         <Laptop size={16} />
       </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<button
onClick={() => setMode('light')}
className={`flex h-8 w-8 items-center justify-center rounded-xl transition-all ${
mode === 'light'
? 'bg-[var(--fs-primary)] text-white shadow-md'
: 'text-[var(--fs-text-muted)] hover:bg-[var(--fs-primary)]/10 hover:text-[var(--fs-primary)]'
}`}
title="Light Mode"
>
<Sun size={16} />
</button>
<button
onClick={() => setMode('dark')}
className={`flex h-8 w-8 items-center justify-center rounded-xl transition-all ${
mode === 'dark'
? 'bg-[var(--fs-primary)] text-white shadow-md'
: 'text-[var(--fs-text-muted)] hover:bg-[var(--fs-primary)]/10 hover:text-[var(--fs-primary)]'
}`}
title="Dark Mode"
>
<Moon size={16} />
</button>
<button
onClick={() => setMode('system')}
className={`flex h-8 w-8 items-center justify-center rounded-xl transition-all ${
mode === 'system'
? 'bg-[var(--fs-primary)] text-white shadow-md'
: 'text-[var(--fs-text-muted)] hover:bg-[var(--fs-primary)]/10 hover:text-[var(--fs-primary)]'
}`}
title="System Preference"
>
<Laptop size={16} />
</button>
<button
type="button"
onClick={() => setMode('light')}
aria-label="Light mode"
aria-pressed={mode === 'light'}
className={`flex h-8 w-8 items-center justify-center rounded-xl transition-all ${
mode === 'light'
? 'bg-[var(--fs-primary)] text-white shadow-md'
: 'text-[var(--fs-text-muted)] hover:bg-[var(--fs-primary)]/10 hover:text-[var(--fs-primary)]'
}`}
title="Light Mode"
>
<Sun size={16} />
</button>
<button
type="button"
onClick={() => setMode('dark')}
aria-label="Dark mode"
aria-pressed={mode === 'dark'}
className={`flex h-8 w-8 items-center justify-center rounded-xl transition-all ${
mode === 'dark'
? 'bg-[var(--fs-primary)] text-white shadow-md'
: 'text-[var(--fs-text-muted)] hover:bg-[var(--fs-primary)]/10 hover:text-[var(--fs-primary)]'
}`}
title="Dark Mode"
>
<Moon size={16} />
</button>
<button
type="button"
onClick={() => setMode('system')}
aria-label="System theme"
aria-pressed={mode === 'system'}
className={`flex h-8 w-8 items-center justify-center rounded-xl transition-all ${
mode === 'system'
? 'bg-[var(--fs-primary)] text-white shadow-md'
: 'text-[var(--fs-text-muted)] hover:bg-[var(--fs-primary)]/10 hover:text-[var(--fs-primary)]'
}`}
title="System Preference"
>
<Laptop size={16} />
</button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/theme/ThemeToggle.tsx` around lines 12 - 44, The
three icon-only buttons that call setMode('light'|'dark'|'system') should be
made accessible: add type="button", an explicit aria-label (e.g., "Light mode",
"Dark mode", "System preference") and an aria-pressed attribute that reflects
the current mode (aria-pressed={mode === 'light' | 'dark' | 'system'}
respectively). Update the buttons that render Sun, Moon and Laptop so their
aria-labels and aria-pressed state match the value passed to setMode to ensure
screen readers see the name and active state.


if (!mounted) return null;

return createPortal(children, document.body);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Portal target breaks theme scoping for modals in light mode.

createPortal(children, document.body) mounts outside the data-theme container, so portaled UI can ignore light-theme overrides and render with root (dark) variables.

🎯 Suggested fix
 export function Portal({ children }: { children: ReactNode }) {
-  const [mounted, setMounted] = useState(false);
+  const [container, setContainer] = useState<HTMLElement | null>(null);
 
   useEffect(() => {
-    setMounted(true);
-    return () => setMounted(false);
+    const themedRoot = document.querySelector<HTMLElement>('[data-theme]');
+    setContainer(themedRoot ?? document.body);
+    return () => setContainer(null);
   }, []);
 
-  if (!mounted) return null;
+  if (!container) return null;
 
-  return createPortal(children, document.body);
+  return createPortal(children, container);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/ui/Portal.tsx` at line 16, The Portal currently uses
createPortal(children, document.body) which breaks theme scoping because it
mounts outside the element that holds data-theme; update the Portal component to
locate the nearest theme-scoped container (e.g., the closest ancestor or
document.querySelector('[data-theme]')) and mount the portal into that element
(or create and append a dedicated wrapper div inside that container) instead of
document.body so modal/content inherits the correct light-theme CSS variables;
update references in the Portal code where createPortal is called to use the
found themeContainer.

Add database services startup, migration, and Playwright installation to CI pipeline.
Configure Playwright for api and web test projects with web server setup.
Add invalidateUserDashboardCache method in RedisService.
Enhance getDashboard in GroupsService to fetch user groups.
Add dashboard.spec.ts e2e test file and test-results directory.
@Arun-kushwaha007

Copy link
Copy Markdown
Owner Author

@copilot review this pr

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
tests/e2e/dashboard.spec.ts (2)

27-36: Consider more specific assertions for dashboard verification.

The assertion getByText('group', { exact: false }) is quite broad and could match unintended elements (e.g., navigation items, any text containing "group"). A more targeted selector would make the test less fragile.

♻️ Suggested improvement
      // The user doesn't have any groups yet, so check for empty state
-      await expect(page.getByText('group', { exact: false }).first()).toBeVisible();
+      // Use a more specific selector for the empty state component
+      await expect(page.getByRole('heading', { name: /no groups/i })).toBeVisible();
+      // Or target a data-testid if available:
+      // await expect(page.getByTestId('empty-groups-state')).toBeVisible();

If the exact empty state text varies, consider adding a data-testid attribute to the empty state component in the dashboard page.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/dashboard.spec.ts` around lines 27 - 36, The test step "Verify
Dashboard Elements" uses a broad locator page.getByText('group', { exact: false
}) which can match unintended elements; update the assertion to target the
empty-state component specifically (e.g., use a data-testid on the empty-state
element and assert with page.getByTestId('dashboard-empty-state') or a more
specific locator like
page.locator('selector-for-empty-state').getByText('expected empty text')),
replacing the existing page.getByText call in the Verify Dashboard Elements step
and adding a data-testid to the dashboard empty-state component if the exact
text can vary.

10-15: Test data is not cleaned up after the test.

Each test run creates a new user in the database that persists after the test completes. Over time, this could accumulate significant test data. Consider adding cleanup in an afterEach hook or using a test-specific database that gets reset between CI runs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/dashboard.spec.ts` around lines 10 - 15, The test seeds a
persistent user via request.post to `${apiBaseUrl}/auth/register` (see
apiBaseUrl and registerResp) but never cleans it up; add an afterEach hook that
removes the created user (e.g., call the API delete user endpoint or a test-only
cleanup endpoint using request or invoke a DB reset helper) using the same email
returned/used by registerResp, or switch the suite to use a disposable/test
database that is reset between runs; ensure the cleanup runs even on failed
assertions so tests do not accumulate users.
.github/workflows/ci.yml (1)

75-82: Consider adding Docker Compose cleanup.

If E2E tests fail or the workflow is cancelled, Docker Compose services remain running. While GitHub-hosted runners are ephemeral, adding cleanup improves consistency and is good practice for self-hosted runners.

♻️ Suggested cleanup step
      - name: Run E2E Tests
        env:
          CI: true
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/fairshare?schema=public
          JWT_SECRET: supersecret
          STRIPE_SECRET_KEY: test_key
          NEXT_PUBLIC_APP_URL: http://localhost:3000
        run: pnpm run e2e

+     - name: Stop Database Services
+       if: always()
+       run: docker compose down
+
      - name: Upload coverage
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 75 - 82, The "Run E2E Tests" job step
currently starts services but lacks cleanup; add a separate always-run cleanup
step (e.g., named "Teardown Docker Compose" or similar) that runs after the "Run
E2E Tests" step using if: always() and executes a docker compose down --volumes
--remove-orphans (or docker-compose down --volumes --remove-orphans) to stop
containers and remove volumes/orphans; reference the existing step name "Run E2E
Tests" and the command "pnpm run e2e" so the teardown executes regardless of
test success/failure or cancellation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 62-70: The CI runs migrations immediately after the "Start
Database Services" step so the Postgres container may not be ready; add a
health/wait step between the "Start Database Services" and "Migrate Database"
steps to ensure readiness. Either change the start command to use docker compose
up -d --wait (if healthchecks exist) or add a separate step that polls the
DATABASE_URL (e.g., using pg_isready, wait-for-it, or a small loop against
localhost:5432) and only proceeds when the DB is accepting connections before
running the pnpm --filter backend prisma:migrate command.

In `@apps/backend/src/groups/groups.service.ts`:
- Around line 701-702: The code currently invalidates only the actor's dashboard
cache after group changes; update the logic in GroupsService (around the calls
to invalidateGroupCache and invalidateUserDashboardCache) to also invalidate
dashboard caches for all other group members: fetch the member IDs for the
affected group (use the existing groupId/group membership retrieval method in
this service), then call this.redis.invalidateUserDashboardCache(memberId) for
each member (exclude or include actorUserId as desired), batching or using Redis
pipeline if available to avoid N+1 latency; keep the existing await
this.redis.invalidateGroupCache(groupId) call and ensure errors are
handled/logged.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 75-82: The "Run E2E Tests" job step currently starts services but
lacks cleanup; add a separate always-run cleanup step (e.g., named "Teardown
Docker Compose" or similar) that runs after the "Run E2E Tests" step using if:
always() and executes a docker compose down --volumes --remove-orphans (or
docker-compose down --volumes --remove-orphans) to stop containers and remove
volumes/orphans; reference the existing step name "Run E2E Tests" and the
command "pnpm run e2e" so the teardown executes regardless of test
success/failure or cancellation.

In `@tests/e2e/dashboard.spec.ts`:
- Around line 27-36: The test step "Verify Dashboard Elements" uses a broad
locator page.getByText('group', { exact: false }) which can match unintended
elements; update the assertion to target the empty-state component specifically
(e.g., use a data-testid on the empty-state element and assert with
page.getByTestId('dashboard-empty-state') or a more specific locator like
page.locator('selector-for-empty-state').getByText('expected empty text')),
replacing the existing page.getByText call in the Verify Dashboard Elements step
and adding a data-testid to the dashboard empty-state component if the exact
text can vary.
- Around line 10-15: The test seeds a persistent user via request.post to
`${apiBaseUrl}/auth/register` (see apiBaseUrl and registerResp) but never cleans
it up; add an afterEach hook that removes the created user (e.g., call the API
delete user endpoint or a test-only cleanup endpoint using request or invoke a
DB reset helper) using the same email returned/used by registerResp, or switch
the suite to use a disposable/test database that is reset between runs; ensure
the cleanup runs even on failed assertions so tests do not accumulate users.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: febf51a0-b37f-44df-a540-812fd56cff7a

📥 Commits

Reviewing files that changed from the base of the PR and between 73b5fea and 0abf10e.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • apps/backend/src/groups/groups.service.ts
  • apps/backend/src/redis/redis.service.ts
  • playwright.config.ts
  • test-results/.last-run.json
  • tests/e2e/dashboard.spec.ts
✅ Files skipped from review due to trivial changes (1)
  • test-results/.last-run.json

Comment thread .github/workflows/ci.yml Outdated
Comment on lines +62 to +70
- name: Start Database Services
run: docker compose up -d

- name: Migrate Database
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/fairshare?schema=public
JWT_SECRET: supersecret
STRIPE_SECRET_KEY: test_key
run: pnpm --filter backend prisma:migrate

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add health check before running migrations.

There's no wait between starting Docker Compose services and running migrations. The database may not be ready to accept connections immediately after docker compose up -d returns, which could cause intermittent CI failures.

🛠️ Proposed fix: Add a wait-for-healthy step
      - name: Start Database Services
        run: docker compose up -d

+     - name: Wait for Database
+       run: |
+         timeout 60 bash -c 'until docker compose exec -T postgres pg_isready; do sleep 1; done'
+
      - name: Migrate Database

Alternatively, if your docker-compose.yml has a healthcheck defined, you can use:

run: docker compose up -d --wait
🧰 Tools
🪛 Checkov (3.2.519)

[medium] 67-68: Basic Auth Credentials

(CKV_SECRET_4)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 62 - 70, The CI runs migrations
immediately after the "Start Database Services" step so the Postgres container
may not be ready; add a health/wait step between the "Start Database Services"
and "Migrate Database" steps to ensure readiness. Either change the start
command to use docker compose up -d --wait (if healthchecks exist) or add a
separate step that polls the DATABASE_URL (e.g., using pg_isready, wait-for-it,
or a small loop against localhost:5432) and only proceeds when the DB is
accepting connections before running the pnpm --filter backend prisma:migrate
command.

Comment on lines +701 to +702
await this.redis.invalidateGroupCache(groupId);
await this.redis.invalidateUserDashboardCache(actorUserId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Other group members' dashboard caches are not invalidated.

Only the actor's dashboard cache is invalidated. Other group members will continue seeing the deleted group in their cached dashboards until the cache TTL (120s) expires. Consider invalidating dashboard caches for all group members.

🛠️ Suggested approach
+    // Fetch all member IDs before invalidation
+    const members = await this.prisma.groupMember.findMany({
+      where: { groupId },
+      select: { userId: true },
+    });

     await this.redis.invalidateGroupCache(groupId);
-    await this.redis.invalidateUserDashboardCache(actorUserId);
+    // Invalidate dashboard cache for all members
+    await Promise.all(
+      members.map((m) => this.redis.invalidateUserDashboardCache(m.userId))
+    );

     return { success: true };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await this.redis.invalidateGroupCache(groupId);
await this.redis.invalidateUserDashboardCache(actorUserId);
// Fetch all member IDs before invalidation
const members = await this.prisma.groupMember.findMany({
where: { groupId },
select: { userId: true },
});
await this.redis.invalidateGroupCache(groupId);
// Invalidate dashboard cache for all members
await Promise.all(
members.map((m) => this.redis.invalidateUserDashboardCache(m.userId))
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/groups/groups.service.ts` around lines 701 - 702, The code
currently invalidates only the actor's dashboard cache after group changes;
update the logic in GroupsService (around the calls to invalidateGroupCache and
invalidateUserDashboardCache) to also invalidate dashboard caches for all other
group members: fetch the member IDs for the affected group (use the existing
groupId/group membership retrieval method in this service), then call
this.redis.invalidateUserDashboardCache(memberId) for each member (exclude or
include actorUserId as desired), batching or using Redis pipeline if available
to avoid N+1 latency; keep the existing await
this.redis.invalidateGroupCache(groupId) call and ensure errors are
handled/logged.

- Moved build step before lint and test in CI pipeline
- Changed Prisma migration to db push for faster database setup
- Added build dependency to lint task in turbo.json for proper execution order
Copilot AI and others added 2 commits April 15, 2026 18:53
…l animations, cleanup

- ThemeProvider: restore data-theme application to document elements
- layout.tsx: restore theme init script to prevent FOUC
- groups.service.ts: add existence and already-deleted checks in delete method
- CreateExpenseModal: restore AnimatePresence for exit animations
- ThemeToggle: remove unused imports and variables
- .gitignore: add test-results/ and remove committed artifact

Agent-Logs-Url: https://github.com/Arun-kushwaha007/FairShare/sessions/3067ae7f-0172-4370-8ac6-04de20b46b7c

Co-authored-by: Arun-kushwaha007 <121769639+Arun-kushwaha007@users.noreply.github.com>
…ared-types/src

Agent-Logs-Url: https://github.com/Arun-kushwaha007/FairShare/sessions/3067ae7f-0172-4370-8ac6-04de20b46b7c

Co-authored-by: Arun-kushwaha007 <121769639+Arun-kushwaha007@users.noreply.github.com>

Copilot AI commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

@copilot review this pr

Here's my review of the PR. I found and fixed several issues in commit 79bd94c:

Critical fixes:

  1. Theme switching broken outside DashboardLayoutresolveTheme was renamed but lost the document.documentElement.dataset.theme / document.body.dataset.theme updates. This meant the theme toggle only affected the DashboardLayout wrapper div, not the full page. Restored as resolveAndApplyTheme.

  2. Flash of unstyled content (FOUC) — The inline theme init script was removed from layout.tsx, so on page load users would see the default dark theme flash before the ThemeProvider useEffect kicks in. Restored the init script.

  3. Soft delete missing safety checksgroups.service.ts delete() didn't verify the group exists or check if it was already soft-deleted. A non-existent groupId would cause a raw Prisma error instead of a proper 404. Added NotFoundException guards for both cases.

  4. CreateExpenseModal lost exit animations — The AnimatePresence wrapper was removed and replaced with an early if (!open) return null, which prevents framer-motion from running exit transitions. Restored AnimatePresence with proper exit prop on the motion.div.

Cleanup:

  1. Unused imports in ThemeToggle.tsxresolved, motion, and AnimatePresence were imported but never used.

  2. test-results/.last-run.json committed — Added test-results to .gitignore and removed the artifact from tracking.

  3. Build artifacts committedpackages/shared-types/src/index.{js,d.ts,js.map} were accidentally tracked; removed in 8c86ebd.

Additional observation (not fixed, pre-existing): The CI workflow uses prisma db push which is meant for prototyping. Consider prisma migrate deploy for a more production-like CI pipeline.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • downloads.sentry-cdn.com
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node node ./scripts/install.js (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

…tions

- Reorder constructor parameters in ReceiptsService tests to match implementation changes
- Add groupId select to expense query expectation in receipts service test
- Include findUnique mock for settlement in integration test setup

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/backend/src/groups/groups.service.ts (1)

705-717: Consider adding activity log and realtime notification for deletion.

Other mutating operations in this service (e.g., create, invite, remindSettlement) create activity records and/or emit realtime events. For audit trail consistency and immediate member notification, consider:

  1. Creating an activity record for the deletion
  2. Emitting a realtime event (e.g., group_deleted) so connected members are informed immediately
💡 Optional enhancement
     await this.prisma.group.update({
       where: { id: groupId },
       data: { 
         deletedAt: new Date(),
         shareEnabled: false,
         shareToken: null
       },
     });

+    await this.prisma.activity.create({
+      data: {
+        groupId,
+        actorUserId,
+        type: 'group_deleted',
+        entityId: groupId,
+        metadata: {},
+      },
+    });
+
     await this.redis.invalidateGroupCache(groupId);
     await this.redis.invalidateUserDashboardCache(actorUserId);

+    this.realtime.emitToGroup(groupId, 'group_deleted', { groupId });
+
     return { success: true };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/groups/groups.service.ts` around lines 705 - 717, The
deletion flow currently updates the group and invalidates caches but lacks an
activity audit and realtime notification; after the prisma update and before
returning, create an activity record (e.g., call this.activity.createActivity or
this.activityService.create with actorUserId, groupId, action: 'group_deleted',
and metadata) and emit a realtime event to members (e.g., this.realtime.emit or
this.realtimeGateway.emit('group_deleted', { groupId, actorUserId })) so
connected clients are notified, then continue to call
this.redis.invalidateGroupCache(groupId) and
this.redis.invalidateUserDashboardCache(actorUserId) and return the existing {
success: true } response.
.github/workflows/ci.yml (1)

65-82: Consider consolidating duplicated environment variables.

The DATABASE_URL, JWT_SECRET, and STRIPE_SECRET_KEY variables are duplicated across the "Migrate Database" and "Run E2E Tests" steps. Defining them once at the job level would reduce duplication and make maintenance easier.

♻️ Suggested refactor using job-level env
   validate:
     runs-on: ubuntu-latest
     timeout-minutes: 30
+    env:
+      DATABASE_URL: postgresql://postgres:postgres@localhost:5432/fairshare?schema=public
+      JWT_SECRET: supersecret
+      STRIPE_SECRET_KEY: test_key

     steps:
       # ... earlier steps ...

       - name: Migrate Database
-        env:
-          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/fairshare?schema=public
-          JWT_SECRET: supersecret
-          STRIPE_SECRET_KEY: test_key
         run: pnpm --filter backend exec prisma db push

       # ... Playwright install ...

       - name: Run E2E Tests
         env:
           CI: true
-          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/fairshare?schema=public
-          JWT_SECRET: supersecret
-          STRIPE_SECRET_KEY: test_key
           NEXT_PUBLIC_APP_URL: http://localhost:3000
         run: pnpm run e2e
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 65 - 82, Move the duplicated
environment variables (DATABASE_URL, JWT_SECRET, STRIPE_SECRET_KEY and any
others like NEXT_PUBLIC_APP_URL/CI if applicable) from the individual steps
"Migrate Database" and "Run E2E Tests" into the job-level env block so they are
defined once for the job; update or remove the per-step env maps in those steps
("Migrate Database" and "Run E2E Tests") so they inherit the job-level env,
ensuring pnpm --filter backend exec prisma db push and pnpm run e2e continue to
run unchanged with the shared variables.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/app/layout.tsx`:
- Around line 12-19: The try block that reads storageKey/stored/mode and sets
document.documentElement.dataset.theme can throw and currently the empty catch
swallows errors leaving no data-theme; update the catch to set a safe fallback
theme (e.g., 'light' or 'system') on document.documentElement.dataset.theme and
optionally log the error (capture the exception object) so failures are visible;
ensure the logic still resolves based on prefersDark/mode when available and
that the catch covers both localStorage and matchMedia failures.

In `@apps/web/src/components/theme/ThemeProvider.tsx`:
- Line 34: The component initializes the local state const [resolved,
setResolved] = useState<ResolvedTheme>('dark') which can mismatch the theme
already applied on document.documentElement and cause a flash; update
ThemeProvider to derive the initial resolved value from the DOM (e.g., read
document.documentElement.dataset.theme or computed style) when creating the
state (or in an effect that runs once) so resolved reflects the pre-hydration
theme; modify the useState initialization or add a useEffect that reads
document.documentElement.dataset.theme, validates it against ResolvedTheme
values, and calls setResolved(...) to avoid the first-paint mismatch.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 65-82: Move the duplicated environment variables (DATABASE_URL,
JWT_SECRET, STRIPE_SECRET_KEY and any others like NEXT_PUBLIC_APP_URL/CI if
applicable) from the individual steps "Migrate Database" and "Run E2E Tests"
into the job-level env block so they are defined once for the job; update or
remove the per-step env maps in those steps ("Migrate Database" and "Run E2E
Tests") so they inherit the job-level env, ensuring pnpm --filter backend exec
prisma db push and pnpm run e2e continue to run unchanged with the shared
variables.

In `@apps/backend/src/groups/groups.service.ts`:
- Around line 705-717: The deletion flow currently updates the group and
invalidates caches but lacks an activity audit and realtime notification; after
the prisma update and before returning, create an activity record (e.g., call
this.activity.createActivity or this.activityService.create with actorUserId,
groupId, action: 'group_deleted', and metadata) and emit a realtime event to
members (e.g., this.realtime.emit or this.realtimeGateway.emit('group_deleted',
{ groupId, actorUserId })) so connected clients are notified, then continue to
call this.redis.invalidateGroupCache(groupId) and
this.redis.invalidateUserDashboardCache(actorUserId) and return the existing {
success: true } response.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 40551356-4c8f-4080-8d90-f5088041d640

📥 Commits

Reviewing files that changed from the base of the PR and between 0abf10e and 2ed04b9.

📒 Files selected for processing (10)
  • .github/workflows/ci.yml
  • .gitignore
  • apps/backend/src/groups/groups.service.ts
  • apps/backend/src/receipts/receipts.service.spec.ts
  • apps/backend/src/settlements/settlements.integration.spec.ts
  • apps/web/app/layout.tsx
  • apps/web/src/components/groups/CreateExpenseModal.tsx
  • apps/web/src/components/theme/ThemeProvider.tsx
  • apps/web/src/components/theme/ThemeToggle.tsx
  • turbo.json
✅ Files skipped from review due to trivial changes (1)
  • .gitignore
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/web/src/components/theme/ThemeToggle.tsx
  • apps/web/src/components/groups/CreateExpenseModal.tsx

Comment thread apps/web/app/layout.tsx Outdated
Comment on lines +12 to +19
try {
const storageKey = 'fs-theme';
const stored = localStorage.getItem(storageKey);
const mode = stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system';
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const resolved = mode === 'system' ? (prefersDark ? 'dark' : 'light') : mode;
var storageKey = 'fs-theme';
var stored = localStorage.getItem(storageKey);
var mode = stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system';
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
var resolved = mode === 'system' ? (prefersDark ? 'dark' : 'light') : mode;
document.documentElement.dataset.theme = resolved;
document.body.dataset.theme = resolved;
} catch (_) {
document.documentElement.dataset.theme = 'light';
document.body.dataset.theme = 'light';
}
} catch (_) {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t swallow init failures without applying a fallback theme.

With Line 44 removing the default data-theme, an empty catch on Line 19 can leave the page without a theme attribute if localStorage/matchMedia access throws.

🛠️ Proposed fix
 (function() {
   try {
     var storageKey = 'fs-theme';
     var stored = localStorage.getItem(storageKey);
     var mode = stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system';
     var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
     var resolved = mode === 'system' ? (prefersDark ? 'dark' : 'light') : mode;
     document.documentElement.dataset.theme = resolved;
-  } catch (_) {}
+  } catch (_) {
+    document.documentElement.dataset.theme = 'light';
+  }
 })();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const storageKey = 'fs-theme';
const stored = localStorage.getItem(storageKey);
const mode = stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system';
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const resolved = mode === 'system' ? (prefersDark ? 'dark' : 'light') : mode;
var storageKey = 'fs-theme';
var stored = localStorage.getItem(storageKey);
var mode = stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system';
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
var resolved = mode === 'system' ? (prefersDark ? 'dark' : 'light') : mode;
document.documentElement.dataset.theme = resolved;
document.body.dataset.theme = resolved;
} catch (_) {
document.documentElement.dataset.theme = 'light';
document.body.dataset.theme = 'light';
}
} catch (_) {}
(function() {
try {
var storageKey = 'fs-theme';
var stored = localStorage.getItem(storageKey);
var mode = stored === 'light' || stored === 'dark' || stored === 'system' ? stored : 'system';
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
var resolved = mode === 'system' ? (prefersDark ? 'dark' : 'light') : mode;
document.documentElement.dataset.theme = resolved;
} catch (_) {
document.documentElement.dataset.theme = 'light';
}
})();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/layout.tsx` around lines 12 - 19, The try block that reads
storageKey/stored/mode and sets document.documentElement.dataset.theme can throw
and currently the empty catch swallows errors leaving no data-theme; update the
catch to set a safe fallback theme (e.g., 'light' or 'system') on
document.documentElement.dataset.theme and optionally log the error (capture the
exception object) so failures are visible; ensure the logic still resolves based
on prefersDark/mode when available and that the catch covers both localStorage
and matchMedia failures.

export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [mode, setModeState] = useState<ThemeMode>('system');
const [resolved, setResolved] = useState<ResolvedTheme>('light');
const [resolved, setResolved] = useState<ResolvedTheme>('dark');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Initialize resolved from existing DOM theme to avoid first-paint mismatch.

Line 34 defaults resolved to 'dark', which can briefly conflict with the pre-hydration theme set in apps/web/app/layout.tsx (script sets document.documentElement.dataset.theme), causing a short flicker for light users.

🎯 Proposed fix
-  const [resolved, setResolved] = useState<ResolvedTheme>('dark');
+  const [resolved, setResolved] = useState<ResolvedTheme>(() => {
+    if (typeof document !== 'undefined') {
+      const initial = document.documentElement.dataset.theme;
+      if (initial === 'light' || initial === 'dark') return initial;
+    }
+    return 'light';
+  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const [resolved, setResolved] = useState<ResolvedTheme>('dark');
const [resolved, setResolved] = useState<ResolvedTheme>(() => {
if (typeof document !== 'undefined') {
const initial = document.documentElement.dataset.theme;
if (initial === 'light' || initial === 'dark') return initial;
}
return 'light';
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/theme/ThemeProvider.tsx` at line 34, The component
initializes the local state const [resolved, setResolved] =
useState<ResolvedTheme>('dark') which can mismatch the theme already applied on
document.documentElement and cause a flash; update ThemeProvider to derive the
initial resolved value from the DOM (e.g., read
document.documentElement.dataset.theme or computed style) when creating the
state (or in an effect that runs once) so resolved reflects the pre-hydration
theme; modify the useState initialization or add a useEffect that reads
document.documentElement.dataset.theme, validates it against ResolvedTheme
values, and calls setResolved(...) to avoid the first-paint mismatch.

…tion script

- Remove theme initialization script from layout.tsx
- Refactor ThemeProvider to only resolve themes without applying DOM changes
- Hardcode dark background and text colors in globals.css
- Enhance ThemeToggle with framer-motion animations and resolved theme usage

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/src/components/theme/ThemeProvider.tsx (1)

55-60: ⚠️ Potential issue | 🟠 Major

Theme resolution is disconnected from guaranteed DOM application.

Line 60 updates React state only. But the stylesheet contract depends on a data-theme attribute selector (apps/web/src/design/theme.ts Line 91-101). In provided context, only apps/web/src/components/layout/DashboardLayout.tsx Line 12-18 applies data-theme={resolved}, so routes/layouts not doing this will stay on default styling.

🔧 Proposed fix (provider-level guarantee)
+  useEffect(() => {
+    document.documentElement.dataset.theme = resolved;
+  }, [resolved]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/theme/ThemeProvider.tsx` around lines 55 - 60, The
ThemeProvider's setMode currently only updates React state (setModeState and
setResolved) and localStorage (STORAGE_KEY), but never guarantees the document's
data-theme attribute used by CSS selectors (resolveTheme) is applied to the DOM;
update setMode to also set document.documentElement.setAttribute('data-theme',
<resolved value>) after calling setResolved(resolveTheme(next)), and ensure the
provider also applies the current resolved theme to document.documentElement on
mount (e.g., in an effect that reads resolveTheme(modeState or stored value)) so
routes/layouts that don't set data-theme still get correct styles.
♻️ Duplicate comments (1)
apps/web/src/components/theme/ThemeProvider.tsx (1)

30-42: ⚠️ Potential issue | 🟡 Minor

Initialize resolved from the initial persisted/system mode to avoid first-render mismatch.

Line 30 starts resolved as 'dark', then Line 41 corrects it after mount. That can briefly render the wrong theme for users with stored 'light' or 'system'+light preference.

🎯 Proposed fix
-  const [resolved, setResolved] = useState<ResolvedTheme>('dark');
+  const [resolved, setResolved] = useState<ResolvedTheme>(() => {
+    if (typeof window === 'undefined') return 'dark';
+    const raw = localStorage.getItem(STORAGE_KEY);
+    const initialMode: ThemeMode =
+      raw === 'light' || raw === 'dark' || raw === 'system' ? raw : 'system';
+    return resolveTheme(initialMode);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/theme/ThemeProvider.tsx` around lines 30 - 42,
resolved is initialized to the literal 'dark' causing a flash before useEffect
runs; change initialization so resolved uses the same initial persisted/system
calculation as readStored + resolveTheme (i.e., compute initial mode via
readStored() or equivalent and pass resolveTheme(initialMode) to useState) and
keep setResolved/useEffect as-is; update references to readStored, resolveTheme,
resolved, and setResolved to ensure consistent initial value on first render.
🧹 Nitpick comments (1)
apps/web/app/globals.css (1)

45-46: Avoid conflicting hover shadow declarations in the same rule.

Line 45 sets box-shadow: var(--fs-shadow-elevated) but Line 46 adds shadow-xl via @apply, which can override it. Keep one source of truth to avoid unpredictable styling drift.

♻️ Proposed cleanup
  .marketing-card:hover {
    box-shadow: var(--fs-shadow-elevated);
-    `@apply` -translate-y-1 border-[var(--fs-border)] shadow-xl;
+    `@apply` -translate-y-1 border-[var(--fs-border)];
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/globals.css` around lines 45 - 46, This rule currently declares
box-shadow: var(--fs-shadow-elevated) and also applies shadow-xl via `@apply` (in
the same selector), causing conflicting shadow declarations; pick a single
source of truth and remove one of them — either delete shadow-xl from the `@apply`
list (-translate-y-1 border-[var(--fs-border)] shadow-xl) and keep box-shadow:
var(--fs-shadow-elevated), or remove the explicit box-shadow line and rely on
`@apply` shadow-xl; update the selector in apps/web/app/globals.css accordingly so
only one shadow is defined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/app/globals.css`:
- Around line 147-148: The .grid-bg rule uses element-level opacity which makes
all children translucent; replace that approach by removing the opacity property
from the .grid-bg selector and instead add alpha channels to the gradient color
stops (e.g., use rgba()/hex with alpha or CSS color-mix for --fs-border) so only
the grid lines are semi-transparent while text and child elements retain full
opacity; update the CSS gradients in the .grid-bg rule (the two linear-gradient
declarations) to use transparentized versions of var(--fs-border) rather than
relying on opacity.
- Around line 20-21: The body has hardcoded colors (background-color: `#030303`;
color: white) that override theme variables and block theme switching; replace
these hardcoded values with the theme CSS variables (use var(--fs-background)
for background and var(--fs-text-primary) for text) and remove any conflicting
static colors so light/dark themes driven by --fs-background / --fs-text-primary
can take effect (add sensible fallbacks inside the var(...) if needed).

---

Outside diff comments:
In `@apps/web/src/components/theme/ThemeProvider.tsx`:
- Around line 55-60: The ThemeProvider's setMode currently only updates React
state (setModeState and setResolved) and localStorage (STORAGE_KEY), but never
guarantees the document's data-theme attribute used by CSS selectors
(resolveTheme) is applied to the DOM; update setMode to also set
document.documentElement.setAttribute('data-theme', <resolved value>) after
calling setResolved(resolveTheme(next)), and ensure the provider also applies
the current resolved theme to document.documentElement on mount (e.g., in an
effect that reads resolveTheme(modeState or stored value)) so routes/layouts
that don't set data-theme still get correct styles.

---

Duplicate comments:
In `@apps/web/src/components/theme/ThemeProvider.tsx`:
- Around line 30-42: resolved is initialized to the literal 'dark' causing a
flash before useEffect runs; change initialization so resolved uses the same
initial persisted/system calculation as readStored + resolveTheme (i.e., compute
initial mode via readStored() or equivalent and pass resolveTheme(initialMode)
to useState) and keep setResolved/useEffect as-is; update references to
readStored, resolveTheme, resolved, and setResolved to ensure consistent initial
value on first render.

---

Nitpick comments:
In `@apps/web/app/globals.css`:
- Around line 45-46: This rule currently declares box-shadow:
var(--fs-shadow-elevated) and also applies shadow-xl via `@apply` (in the same
selector), causing conflicting shadow declarations; pick a single source of
truth and remove one of them — either delete shadow-xl from the `@apply` list
(-translate-y-1 border-[var(--fs-border)] shadow-xl) and keep box-shadow:
var(--fs-shadow-elevated), or remove the explicit box-shadow line and rely on
`@apply` shadow-xl; update the selector in apps/web/app/globals.css accordingly so
only one shadow is defined.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7a17e1cf-2466-4641-a3cd-02b2e5059d0e

📥 Commits

Reviewing files that changed from the base of the PR and between 2ed04b9 and c73399f.

📒 Files selected for processing (4)
  • apps/web/app/globals.css
  • apps/web/app/layout.tsx
  • apps/web/src/components/theme/ThemeProvider.tsx
  • apps/web/src/components/theme/ThemeToggle.tsx
✅ Files skipped from review due to trivial changes (1)
  • apps/web/app/layout.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web/src/components/theme/ThemeToggle.tsx

Comment thread apps/web/app/globals.css
Comment on lines +20 to +21
background-color: #030303;
color: white;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded body colors break theme switching behavior.

Lines 20-21 force dark colors (#030303 / white) and bypass --fs-background and --fs-text-primary, so light mode cannot render correctly even when theme state resolves to light.

🔧 Proposed fix
  body {
-    background-color: `#030303`;
-    color: white;
+    background-color: var(--fs-background);
+    color: var(--fs-text-primary);
     font-family: var(--fs-font-sans);
     `@apply` transition-colors duration-300;
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/globals.css` around lines 20 - 21, The body has hardcoded colors
(background-color: `#030303`; color: white) that override theme variables and
block theme switching; replace these hardcoded values with the theme CSS
variables (use var(--fs-background) for background and var(--fs-text-primary)
for text) and remove any conflicting static colors so light/dark themes driven
by --fs-background / --fs-text-primary can take effect (add sensible fallbacks
inside the var(...) if needed).

Comment thread apps/web/app/globals.css
Comment on lines +147 to +148
linear-gradient(to right, var(--fs-border) 1px, transparent 1px),
linear-gradient(to bottom, var(--fs-border) 1px, transparent 1px);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

opacity on .grid-bg will fade all child content, not just the grid lines.

Line 150 applies element-level opacity, so any text/content inside a .grid-bg container becomes translucent and may hurt readability/contrast. Prefer encoding transparency in the gradient colors instead of the whole element.

🔧 Proposed fix
  .grid-bg {
    background-image:
-      linear-gradient(to right, var(--fs-border) 1px, transparent 1px),
-      linear-gradient(to bottom, var(--fs-border) 1px, transparent 1px);
+      linear-gradient(to right, color-mix(in srgb, var(--fs-border) 15%, transparent) 1px, transparent 1px),
+      linear-gradient(to bottom, color-mix(in srgb, var(--fs-border) 15%, transparent) 1px, transparent 1px);
    background-size: 40px 40px;
-    opacity: 0.15;
  }

Also applies to: 150-150

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/globals.css` around lines 147 - 148, The .grid-bg rule uses
element-level opacity which makes all children translucent; replace that
approach by removing the opacity property from the .grid-bg selector and instead
add alpha channels to the gradient color stops (e.g., use rgba()/hex with alpha
or CSS color-mix for --fs-border) so only the grid lines are semi-transparent
while text and child elements retain full opacity; update the CSS gradients in
the .grid-bg rule (the two linear-gradient declarations) to use transparentized
versions of var(--fs-border) rather than relying on opacity.

…lean import

- Remove BOM from React import
- Change test assertion to check for 'Sign In' instead of 'Login'
…ables

- Updated .env.example and added apps/backend/.env.example with new variables
- Modified CI workflow to include SUPABASE_DATABASE_URL and DIRECT_URL for database migration and testing jobs
- Added JWT_REFRESH_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, REDIS_URL, AWS_REGION, and S3_BUCKET
- Updated STRIPE_SECRET_KEY to use test key format for consistency in CI pipeline
- Added NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, and NEXT_PUBLIC_API_URL to CI e2e job
- Updated .env.example with NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY placeholders for local development consistency
@coderabbitai

coderabbitai Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

Fixes Applied Successfully

Fixed 9 file(s) based on 9 unresolved review comments.

Files modified:

  • apps/backend/src/balances/balances.service.ts
  • apps/backend/src/groups/groups.service.ts
  • apps/web/app/dashboard/page.tsx
  • apps/web/src/components/groups/CreateExpenseModal.tsx
  • apps/web/src/components/groups/CreateGroupModal.tsx
  • apps/web/src/components/groups/DeleteGroupModal.tsx
  • apps/web/src/components/theme/ThemeToggle.tsx
  • apps/web/src/components/ui/Portal.tsx
  • tsconfig.base.json

Commit: 8e0f4a0943a09e76581822d1e5df9afa4f84adaa

The changes have been pushed to the dev branch.

Time taken: 8m 53s

@Arun-kushwaha007
Arun-kushwaha007 merged commit acbe687 into main Apr 16, 2026
4 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.

2 participants