Skip to content

Gym enhancements contrast chat - #178

Closed
khaled7172 wants to merge 7 commits into
gym-sharifa-mhmdharkous-khaledfrom
gym-enhancements-contrast-chat
Closed

Gym enhancements contrast chat#178
khaled7172 wants to merge 7 commits into
gym-sharifa-mhmdharkous-khaledfrom
gym-enhancements-contrast-chat

Conversation

@khaled7172

@khaled7172 khaled7172 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Description

• Member Portal Dark-Mode Overhaul: Replaced all hardcoded grays/whites (bg-white, border-gray-200, text-gray-900, bg-gray-50, etc.) across MemberSidebar, PortalHomePage, MyBookingsPage,
AvailablePlansPage, MySubscriptionsPage, MyProfilePage, and DeactivatedPage with semantic, dark-mode aware tokens (bg-card, border-border, text-foreground, text-muted-foreground, bg-muted).
• Enhanced Typography & Hierarchy: Standardized heading contrast, duration pills, price tags, and status badges (ACTIVE, EXPIRED, CANCELLED, BOOKED, CHECKED_IN) so text is clearly legible in both dark
and light modes.
• Audit Log UX Polish: Removed redundant top-level counter text (Showing X-Y entries) in favor of bottom pagination and eliminated view flickering when toggling activity log status filters.
• Sleek Activity Feed Layout: Transformed the dashboard RecentActivityCard into a clean, full-width feed with consistent iconography and streamlined typography.

Link to issue or ticket

• Addresses Member Portal dark-mode styling issues and dashboard/audit log UI refinements.

Steps to QA

  1. Log in to the Member Portal as alex.rivera@example.com via magic link or existing session.
  2. Toggle between Light Mode and Dark Mode using the top navbar theme switch:
    • Verify the sidebar, navigation buttons, active states, and account box adapt smoothly without stark white boxes or illegible text.
    • Navigate through /portal (Home), /portal/bookings (My Bookings), /portal/subscriptions (My Subscriptions), /portal/plans (Available Plans), and /portal/profile (My Profile) to confirm every card,
    row, and dialog uses proper semantic tokens and contrast.
  3. Log in as an ORG_ADMIN (e.g., sarah.chen@ironpeakfitness.com) and visit /dashboard and /audit-logs:
    • Confirm the RecentActivityCard on the dashboard renders clean and full width.
    • Switch status tabs in /audit-logs and confirm filtering transitions smoothly without page flickering or redundant counters.

Screenshots

Screenshot from 2026-07-20 19-44-45

Summary by CodeRabbit

  • New Features

    • Added an Activity Log for administrators with filtering and pagination.
    • Added GymFlow Assistant chat for contextual gym and member support.
    • Added dashboard Recent Activity feed and dark mode controls.
    • Added smoother page transitions, animated counters, and loading effects.
  • Enhancements

    • Refreshed GymFlow branding and modernized screens across admin and member portals.
    • Improved audit visibility for key bookings, check-ins, members, plans, subscriptions, and gym settings.
    • Updated outgoing emails to use the GymFlow sender address.
  • Bug Fixes

    • Improved access validation when updating gym settings.
    • Added safer default connectivity behavior for local environments.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds audit logging and AI chat across the API and web apps, introduces dark mode, animations, a GymFlow visual redesign, new audit-log interfaces, updated runtime defaults and email branding, revised seeded accounts, and documentation for the enhancement phases.

Changes

Audit logging

Layer / File(s) Summary
Audit storage, API, and event integration
packages/database/prisma/*, packages/contracts/src/audit-log/*, apps/api/src/audit/*, apps/api/src/{bookings,checkins,gyms,instructors,members,plans,sessions,subscriptions}/*
Adds the AuditLog model and migration, validated list contracts, role-scoped listing, exported audit service, and actor-aware lifecycle logging.
Audit log interfaces
apps/web/app/(authenticated)/audit-logs/page.tsx, apps/web/hooks/use-audit-logs.ts, apps/web/app/(authenticated)/dashboard/page.tsx, apps/web/components/app-sidebar.tsx
Adds filtered pagination, timeline rendering, recent activity, and an organization-admin Activity Log navigation entry.

AI chat

Layer / File(s) Summary
Chat contracts and API
packages/contracts/src/chat/*, apps/api/src/chat/*
Adds validated chat request/response schemas, an authenticated endpoint, role-scoped context construction, OpenAI integration, and rule-based fallback responses.
Chat client experience
apps/web/hooks/use-chat.ts, apps/web/components/chat-widget.tsx, apps/web/app/(authenticated)/layout.tsx, apps/web/app/(member)/layout.tsx
Adds optimistic chat state, suggestions, markdown rendering, input handling, and floating widgets in authenticated layouts.

Web experience and branding

Layer / File(s) Summary
Theme, animation, and shared components
apps/web/app/globals.css, apps/web/hooks/use-dark-mode.ts, apps/web/components/*, apps/web/app/layout.tsx
Adds persisted light/dark/system modes, semantic styling utilities, reduced-motion animations, route transitions, animated counters, and themed shared navigation.
Authenticated and member page redesign
apps/web/app/(authenticated)/*, apps/web/app/(member)/*
Updates cards, badges, tables, typography, empty states, pagination, dashboard interactions, and member portal styling to use the new design tokens.
Runtime, branding, and seeded data
apps/api/src/app.module.ts, apps/api/src/main.ts, apps/api/src/auth/*, apps/api/src/mail/*, apps/web/app/login/page.tsx, packages/database/prisma/seeders/*
Adds environment and Redis defaults, updates GymFlow email and login branding, and aligns seeded account references.
Documentation
docs/*
Adds the enhancement plan, feature progress pages, updated progress overview, and revised test-account recipes.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: mohammad-harkous

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.00% 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 title is vague and doesn’t clearly summarize the main changes across dark mode, audit logs, and chat. Use a concise, specific title that names the primary change, such as adding audit logs, chat, and dark-mode UI updates.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description matches the template with Description, issue/ticket, QA steps, and Screenshots sections, with concrete verification steps.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gym-enhancements-contrast-chat

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install timed out. The project may have too many dependencies for the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Caution

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

⚠️ Outside diff range comments (3)
docs/PROGRESS.md (1)

23-36: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Synchronize enhancement-round status across project-tracking documents.

The index says Features E–I are ready to start, while the plan says “Not started” and the feature progress files report all phases complete. Update both documents to reflect the actual lifecycle state.

  • docs/PROGRESS.md#L23-L36: Change the overall-state sentence to match the completed E–I rows.
  • docs/enhancement-plan.md#L3-L6: Update the plan status from “Not started” or explicitly mark it as an implementation history document.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/PROGRESS.md` around lines 23 - 36, Synchronize the enhancement-round
lifecycle status across both tracking documents: in docs/PROGRESS.md lines
23-36, update the overall-state sentence to indicate Features E–I are complete
rather than ready to start; in docs/enhancement-plan.md lines 3-6, replace “Not
started” with the current completed status or explicitly label the document as
an implementation history document.
apps/api/src/instructors/instructors.service.ts (2)

78-93: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Add missing audit logging for instructor creation.

The actor parameter was added to this method to support the new audit logging requirements, but the actual logging call was omitted. This causes instructor creations to silently bypass the audit log.

🛠️ Proposed fix to emit the audit log
   async create(
     gymId: string,
     dto: InstructorCreateRequest,
     actor: User,
   ): Promise<InstructorResponse> {
-    return this.prisma.instructor.create({
+    const created = await this.prisma.instructor.create({
       data: {
         gymId,
         name: dto.name,
         email: dto.email === '' ? null : (dto.email ?? null),
         specialization:
           dto.specialization === '' ? null : (dto.specialization ?? null),
       },
       select: INSTRUCTOR_SELECT,
     });
+
+    this.auditService
+      .log({
+        gymId,
+        userId: actor.id,
+        userName: actor.name,
+        action: 'instructor.created',
+        entityType: 'Instructor',
+        entityId: created.id,
+        entityName: created.name,
+      })
+      .catch(() => {});
+
+    return created;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/instructors/instructors.service.ts` around lines 78 - 93, Update
the InstructorService create method to emit the required audit log after
successfully creating the instructor, using the provided actor and created
instructor details. Preserve the existing Prisma creation and response behavior
while ensuring every successful instructor creation is recorded.

96-124: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Add missing audit logging and enforce tenant scoping.

Similar to create, the actor parameter was added but never used to log the instructor update.

Additionally, fetching the updated instructor using findUniqueOrThrow({ where: { id } }) explicitly violates the coding guideline to "Use findFirst({ where: { id, organizationId } }) even for findUnique calls" to ensure rigorous cross-tenant safety.

🛠️ Proposed fix to resolve both issues
   async update(
     id: string,
     gymId: string,
     dto: InstructorUpdateRequest,
     actor: User,
   ): Promise<InstructorResponse> {
     const result = await this.prisma.instructor.updateMany({
       where: { id, gymId },
       data: {
         ...(dto.name !== undefined && { name: dto.name }),
         ...(dto.email !== undefined && {
           email: dto.email === '' ? null : dto.email,
         }),
         ...(dto.specialization !== undefined && {
           specialization: dto.specialization === '' ? null : dto.specialization,
         }),
         ...(dto.isActive !== undefined && { isActive: dto.isActive }),
       },
     });
 
     if (result.count === 0) {
       throw new NotFoundException(`Instructor with ID ${id} not found`);
     }
 
-    return this.prisma.instructor.findUniqueOrThrow({
-      where: { id },
+    const updated = await this.prisma.instructor.findFirstOrThrow({
+      where: { id, gymId },
       select: INSTRUCTOR_SELECT,
     });
+
+    const action =
+      dto.isActive === false && updated.isActive === false
+        ? 'instructor.deactivated'
+        : 'instructor.updated';
+
+    this.auditService
+      .log({
+        gymId,
+        userId: actor.id,
+        userName: actor.name,
+        action,
+        entityType: 'Instructor',
+        entityId: updated.id,
+        entityName: updated.name,
+      })
+      .catch(() => {});
+
+    return updated;
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/instructors/instructors.service.ts` around lines 96 - 124,
Update the InstructorService.update method to record an audit event using the
provided actor, following the existing create audit-logging pattern. Change the
final instructor lookup to enforce tenant scoping with gymId (using the
project’s scoped findFirst convention rather than unscoped findUniqueOrThrow),
while preserving the existing response selection and not-found behavior.

Source: Coding guidelines

🧹 Nitpick comments (15)
apps/api/src/chat/chat.service.ts (1)

24-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Declare CHAT_LLM_API_KEY and CHAT_LLM_MODEL in turbo.json.

Static analysis flags both env vars as undeclared. Add them to the relevant task's env in turbo.json so Turborepo cache keys account for them and builds stay deterministic.

Also applies to: 224-224

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/chat/chat.service.ts` at line 24, Add CHAT_LLM_API_KEY and
CHAT_LLM_MODEL to the relevant task’s env configuration in turbo.json, alongside
the existing environment variables, so Turborepo recognizes both values in task
cache keys.

Source: Linters/SAST tools

packages/contracts/src/chat/chat-message.response.ts (1)

3-14: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Split chatSourceSchema into its own file.

This file defines two exported schemas. As per coding guidelines, "Create one schema per file in packages/contracts". Move chatSourceSchema/ChatSource into a dedicated file (e.g. chat-source.response.ts) and import it here, updating the folder index.ts and packages/contracts/src/index.ts accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/contracts/src/chat/chat-message.response.ts` around lines 3 - 14,
Move chatSourceSchema and ChatSource out of chat-message.response.ts into a
dedicated chat-source.response.ts file, then import the schema where
chatMessageResponseSchema defines sources. Update the relevant folder index.ts
and packages/contracts/src/index.ts exports so both schemas remain publicly
available.

Source: Coding guidelines

apps/web/components/chat-widget.tsx (1)

81-88: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Close button lacks an accessible name.

Unlike the FAB (aria-label="Toggle chat") and clear button (title="Clear chat"), the close (X) icon button has no title/aria-label, making its purpose unclear to screen-reader users.

♿ Proposed fix
               <Button
                 variant="ghost"
                 size="icon"
                 onClick={() => setIsOpen(false)}
                 className="h-8 w-8 text-muted-foreground hover:text-foreground"
+                aria-label="Close chat"
               >
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/components/chat-widget.tsx` around lines 81 - 88, Add an accessible
name to the close Button that calls setIsOpen(false), using an aria-label or
title describing the action as closing the chat; leave the existing icon and
styling unchanged.
apps/web/hooks/use-chat.ts (1)

7-14: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

sources from the chat response is never surfaced to the user.

ChatMessageResponse (packages/contracts/src/chat/chat-message.response.ts) optionally returns a sources array for grounding citations, but ChatMessage has no field for it and sendMessage only copies response.reply into state — response.sources is silently discarded, so the assistant's citations are never rendered.

♻️ Proposed fix to thread `sources` through
 export interface ChatMessage {
   id: string;
   role: 'user' | 'assistant';
   content: string;
   timestamp: Date;
   isLoading?: boolean;
   isError?: boolean;
+  sources?: ChatMessageResponse['sources'];
 }
...
       setMessages((prev) =>
         prev.map((msg) =>
           msg.id === assistantMessageId
-            ? { ...msg, content: response.reply, isLoading: false }
+            ? { ...msg, content: response.reply, sources: response.sources, isLoading: false }
             : msg,
         ),
       );

Also applies to: 58-69

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/hooks/use-chat.ts` around lines 7 - 14, Update the ChatMessage model
and sendMessage flow to preserve the optional sources returned by
ChatMessageResponse: add the appropriate sources field to ChatMessage and copy
response.sources onto the assistant message instead of discarding it. Keep
existing reply, loading, and error behavior unchanged so the citations remain
available for rendering.
apps/web/app/globals.css (1)

527-552: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer design tokens over raw hex in skeleton-shimmer, sidebar-gradient, and table-premium.

These new utilities (#f1f2f4, #e9eaec, #323b49, #3d4757, #fafafa, #f5f5f5, #1f2937, #111827, #687588, #a0aec0, #e9eaec) hardcode colors instead of referencing the gray tokens already defined for the theme. This drifts from the rest of the file (which mostly uses var(--primary-*)) and makes future palette/dark-mode adjustments harder to keep consistent.
As per coding guidelines, "Apply design tokens (primary-base, primary-100, gray-*, error) defined in globals.css instead of arbitrary color values."

Also applies to: 564-571, 573-602

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/globals.css` around lines 527 - 552, Replace the raw hex colors
in .skeleton-shimmer, .sidebar-gradient, and .table-premium with the appropriate
existing gray and primary design-token variables defined in globals.css,
including both light and dark theme values. Preserve each utility’s gradients,
contrast, and behavior while ensuring all listed colors use theme tokens rather
than hardcoded values.

Source: Coding guidelines

apps/web/app/(authenticated)/members/[id]/page.tsx (1)

49-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Standardize the StatusBadge implementation.

This component retains a verbose implementation and relies on an external mapping object. To maintain consistency with the rest of the redesign (such as in members/page.tsx and instructors/page.tsx), use the standard badge-pill classes directly.

♻️ Proposed refactor
-const STATUS_COLORS: Record<string, string> = {
-  ACTIVE: 'bg-success/10 text-success border-success/20',
-  INACTIVE: 'bg-muted text-muted-foreground border-border',
-};
-
-function StatusBadge({ status }: { status: string }) {
-  return (
-    <span
-      className={`inline-flex items-center rounded-full border px-3 py-1 text-sm font-medium ${STATUS_COLORS[status] ?? 'bg-muted text-muted-foreground border-border'}`}
-    >
-      {status === 'ACTIVE' ? 'Active' : 'Inactive'}
-    </span>
-  );
-}
+function StatusBadge({ status }: { status: string }) {
+  if (status === 'ACTIVE') {
+    return <span className="badge-pill badge-active">• Active</span>;
+  }
+  return <span className="badge-pill badge-inactive">• Inactive</span>;
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(authenticated)/members/[id]/page.tsx around lines 49 - 62,
Update StatusBadge to remove the external STATUS_COLORS mapping and use the
standard badge-pill classes directly, matching the implementations in the
related members and instructors pages while preserving the existing
active/inactive label behavior.
apps/web/app/(authenticated)/sessions/page.tsx (1)

566-570: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Swap outline-none for outline-hidden. Tailwind v4 treats outline-none as a strict outline removal; outline-hidden preserves a transparent outline so the custom focus ring remains visible in forced-colors/high-contrast modes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(authenticated)/sessions/page.tsx around lines 566 - 570,
Update the Link element rendering session entries to replace the Tailwind class
outline-none with outline-hidden, preserving the existing custom focus ring and
other classes.
apps/web/app/(authenticated)/dashboard/page.tsx (3)

448-502: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

getActionDetails duplicated from audit-logs/page.tsx — shares its entityType-casing bug.

This is a verbatim copy of getActionDetails from apps/web/app/(authenticated)/audit-logs/page.tsx (lines 34-87), so it inherits the same entityType-casing mismatch (entityType === 'CHECK_IN' vs. actual 'CheckIn'). Flagging here; will consolidate into a single recommendation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(authenticated)/dashboard/page.tsx around lines 448 - 502,
Update getActionDetails to handle the actual entityType casing for CheckIn, and
apply the same correction consistently in its duplicate implementation on the
audit logs page. Preserve the existing action and entity-specific icon/color
mappings.

512-526: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Hand-rolled relative-time formatting duplicates date-fns.

formatRelativeTime reimplements what date-fns's formatDistanceToNow already does (and is already used for the same purpose in audit-logs/page.tsx, with date-fns imported elsewhere in this very file for differenceInDays). The hand-rolled version also has a subtle edge case: a future/clock-skewed date produces a negative diffInMinutes, which still satisfies diffInMinutes < 1 and prints "just now" rather than being guarded against.

♻️ Proposed fix
-import { differenceInDays } from 'date-fns';
+import { differenceInDays, formatDistanceToNow } from 'date-fns';
@@
-  const formatRelativeTime = (dateStr: string | Date) => {
-    const date = new Date(dateStr);
-    const now = new Date();
-    const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / 60000);
-    if (diffInMinutes < 1) return 'just now';
-    if (diffInMinutes < 60) return `${diffInMinutes}m ago`;
-    const diffInHours = Math.floor(diffInMinutes / 60);
-    if (diffInHours < 24) return `${diffInHours}h ago`;
-    const diffInDays = Math.floor(diffInHours / 24);
-    if (diffInDays < 7) return `${diffInDays}d ago`;
-    return date.toLocaleDateString('en-US', {
-      month: 'short',
-      day: 'numeric',
-    });
-  };
+  const formatRelativeTime = (dateStr: string | Date) =>
+    formatDistanceToNow(new Date(dateStr), { addSuffix: true });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(authenticated)/dashboard/page.tsx around lines 512 - 526,
Replace the hand-rolled formatRelativeTime implementation with date-fns
formatDistanceToNow, reusing the existing date-fns import in this page. Preserve
the relative-time display behavior while delegating future or clock-skewed date
handling to the library, and remove the redundant local calculation logic.

217-271: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Full-capacity/near-capacity styling uses raw red/amber instead of semantic tokens.

Line 225 (text-red-500 icon) and line 262 (text-red-600 dark:text-red-400 "Gym is full!" text) hardcode red rather than using the error design token the coding guidelines call out for this exact purpose. Flagging for consolidation with a similar instance in app-sidebar.tsx.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(authenticated)/dashboard/page.tsx around lines 217 - 271, The
Live Capacity card in the dashboard uses raw red/amber classes for full and
near-capacity states instead of semantic design tokens. Update the styling in
the capacity icon, progress indicator, and status messages associated with
isFull and isNearFull to use the established error and warning tokens,
preserving the existing state-dependent appearance and layout.

Source: Coding guidelines

apps/web/components/app-sidebar.tsx (1)

238-238: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Logout hover uses raw red instead of the error design token.

As per coding guidelines, "Apply design tokens (primary-base, primary-100, gray-*, error) defined in globals.css instead of arbitrary color values." This hover state hardcodes red-50/red-600/red-900 rather than the error token. Flagging for consolidation with a similar instance in dashboard/page.tsx.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/components/app-sidebar.tsx` at line 238, Update the logout element’s
className to replace the hardcoded red hover background and text classes with
the established error design token classes, matching the token-based styling
used by the similar logout instance in dashboard/page.tsx while preserving the
existing layout and transition classes.

Source: Coding guidelines

apps/api/src/checkins/checkins.service.ts (1)

127-137: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use the member's name for entityName instead of repeating the ID.

Both audit-log calls set entityName: \CheckIn ${id}`, which just repeats the entity's own ID. The CHECKIN_SELECTprojection already includesmember.name, so the audit-log UI (which renders entityName` as the human-readable subject of the activity line) would show a much more useful value.

✏️ Proposed fix
       this.auditService
         .log({
           gymId,
           userId: actor.id,
           userName: actor.name,
           action: 'checkin.created',
           entityType: 'CheckIn',
           entityId: checkIn.id,
-          entityName: `CheckIn ${checkIn.id}`,
+          entityName: checkIn.member?.name ?? `CheckIn ${checkIn.id}`,
         })
         .catch(() => {});
     this.auditService
       .log({
         gymId,
         userId: actor.id,
         userName: actor.name,
         action: 'checkin.checked-out',
         entityType: 'CheckIn',
         entityId: id,
-        entityName: `CheckIn ${id}`,
+        entityName: updated?.member?.name ?? `CheckIn ${id}`,
       })
       .catch(() => {});

Also applies to: 174-184

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/checkins/checkins.service.ts` around lines 127 - 137, Update the
audit-log payloads in the check-in creation flow, including both calls near the
visible log invocation and the corresponding later call, so entityName uses the
selected member.name value instead of the CheckIn ID template. Keep entityId
unchanged.
packages/database/prisma/schema.prisma (1)

314-332: 🔒 Security & Privacy | 🔵 Trivial

Consider a retention/anonymization policy for ipAddress and userName.

AuditLog denormalizes userName and stores raw ipAddress with no expiry, and (unlike Session/MagicLink) it lives in the public schema. Since this data persists indefinitely and isn't tied to the source User record, a user-deletion/erasure request wouldn't purge it. Worth deciding on a retention window or redaction job for these fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/database/prisma/schema.prisma` around lines 314 - 332, Define and
apply a retention or anonymization policy for AuditLog.userName and
AuditLog.ipAddress, including a mechanism to redact or remove these values after
the chosen window and during user-erasure workflows. Keep audit event records
intact while ensuring these denormalized fields are no longer retained
indefinitely.
packages/contracts/src/audit-log/audit-log.response.ts (1)

4-6: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Modernize .uuid() chain to top-level z.uuid() (Zod 4).

z.string().uuid() still works in Zod 4.3.5 but is deprecated in favor of the top-level z.uuid(). Note the new form is stricter (validates RFC 9562/4122 variant bits), so confirm existing seeded/generated UUIDs remain compliant before switching.

Also applies to: 10-10

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/contracts/src/audit-log/audit-log.response.ts` around lines 4 - 6,
Update the UUID schemas in the audit-log response definition, including the
corresponding occurrence around the referenced additional location, from the
deprecated z.string().uuid() form to top-level z.uuid(). Preserve nullable
behavior for gymId and verify existing seeded/generated UUID values comply with
the stricter RFC 9562/4122 variant validation.
apps/web/app/(authenticated)/audit-logs/page.tsx (1)

34-87: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

getActionDetails is duplicated verbatim in dashboard/page.tsx.

This helper (and its entityType-casing bug above) is copy-pasted into apps/web/app/(authenticated)/dashboard/page.tsx (lines 448-502), differing only in the fallback icon. Flagging here; will consolidate into a single recommendation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/`(authenticated)/audit-logs/page.tsx around lines 34 - 87,
Consolidate the duplicated getActionDetails helper shared by the audit logs and
dashboard pages into one reusable implementation, preserving each page’s
required fallback icon behavior. Correct the entityType casing handling within
the shared helper so CHECK_IN, SESSION, BOOKING, and INSTRUCTOR matching works
consistently across both callers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/audit/audit.controller.ts`:
- Around line 43-45: Update the audit controller method containing the gymId
assignment and auditService.list call to import and throw ForbiddenException
when an ORG_ADMIN has no associated gymId; only call auditService.list after
this validation, while preserving SUPER_ADMIN access and existing filtering for
associated users.

In `@apps/api/src/audit/audit.swagger.ts`:
- Line 2: Update the audit Swagger definitions by removing the unused
getSchemaPath import from the ApiOkResponse import and changing the metadata
property in the relevant response class from any | null to unknown (or the
established narrower JSON type), preserving its nullable contract without using
any.

In `@apps/api/src/bookings/bookings.service.ts`:
- Around line 207-211: Implement audit logging for the administrative methods:
in apps/api/src/bookings/bookings.service.ts lines 207-211, update
BookingService.cancel to log booking.cancelled with actor; in
apps/api/src/gyms/gyms.service.ts lines 110-114, 161-165, and 181-185, update
the approve, reject, and suspend methods to log gym.approved, gym.rejected, and
gym.suspended respectively using actor. Update GymsController approve, reject,
and suspend endpoints to inject `@CurrentUser`() user: User and pass it to the
corresponding service methods.

In `@apps/api/src/chat/chat.service.ts`:
- Around line 227-246: Update the fetch call in the chat service to include an
AbortSignal.timeout(15000) signal option, ensuring the LLM request fails after
15 seconds and reaches the existing rule-based fallback handling.

In `@apps/api/src/mail/mail.processor.ts`:
- Line 66: Update the email templates in mail.processor.ts: change the subject
to “Sign in to GymFlow” at lines 66-66 and update the body text to “has invited
you to join ${organizationName} on GymFlow.” at lines 105-105.

In `@apps/api/src/sessions/sessions.service.ts`:
- Around line 107-111: SessionsService mutation methods do not record audit
events. In apps/api/src/sessions/sessions.service.ts at lines 107-111, add a
fire-and-forget AuditService log with action session.created before returning
the created session; at lines 150-155, log session.updated before returning the
updated session; and at lines 214-218, log session.cancelled before returning
the cancelled session, including gymId, actor identity, Session entity details,
and existing logger-based rejection handling.

In `@apps/web/app/`(authenticated)/audit-logs/page.tsx:
- Around line 22-32: Update ENTITY_TYPES values and the entity-type checks in
getActionDetails to match the exact PascalCase entityType strings written by the
backend services, including MembershipPlan and CheckIn. Verify and align
Members, Subscriptions, Sessions, Bookings, Instructors, and Gym against their
service values so filtering and entity-specific icons work correctly.

In `@apps/web/app/`(authenticated)/checkins/page.tsx:
- Around line 111-114: Replace the hardcoded green styling with semantic success
tokens in both sites: update the status indicator in
apps/web/app/(authenticated)/checkins/page.tsx lines 111-114 to use the success
background token, and update the corresponding status styling in
apps/web/app/(authenticated)/sessions/[id]/page.tsx lines 793-796 to use
success-based background, text, and border tokens instead of green and
dark:green classes.

In `@apps/web/app/`(authenticated)/dashboard/page.tsx:
- Around line 505-510: Update RecentActivityCard to destructure and handle the
error returned by useAuditLogs, following the explicit error-state pattern used
by audit-logs/page.tsx. Render the failed-load state before the empty “No
activity yet” state, while preserving the existing loading and successful
activity rendering behavior.

In `@apps/web/app/`(authenticated)/sessions/page.tsx:
- Around line 265-271: Restore the DatePicker minDate constraint in the date
field render block, using the existing intended minimum-date value so past dates
cannot be selected and form validation is prevented before submission.

In `@apps/web/app/globals.css`:
- Around line 703-714: Update the prefers-reduced-motion rule to include the
.btn-pulse selector, disabling its subtle-pulse animation while preserving the
existing opacity and transform overrides and related reduced-motion selectors.
- Around line 619-660: Rename the keyframes fadeSlideUp, fadeIn, scaleIn,
countUp, and slideInRight to kebab-case names, then update every corresponding
animation reference in the same stylesheet so each usage matches its renamed
keyframe.
- Around line 458-511: Update the badge theme styles for .badge-inactive,
.badge-expired, .badge-cancelled, and .badge-scheduled to use existing theme
tokens instead of hardcoded light-mode hex values, and add explicit dark-mode
color declarations in each corresponding .dark selector. Ensure the dark text
colors provide sufficient contrast against their low-opacity dark backgrounds
while preserving the existing active and completed badge behavior.

In `@apps/web/components/app-sidebar.tsx`:
- Around line 79-84: Update the navigation definitions in app-sidebar.tsx so the
Activity Log entry is included in superAdminNavItems as well as the existing
org-admin navigation, reusing the existing item where practical and preserving
its /audit-logs URL, ClipboardList icon, and ORG_ADMIN role configuration.

In `@apps/web/components/chat-widget.tsx`:
- Around line 24-29: Update formatChatMarkdown to escape HTML-sensitive
characters in assistant text before applying markdown formatting, ensuring
msg.content cannot inject markup through dangerouslySetInnerHTML. Also add an
accessible aria-label or title to the close button near the chat widget
controls.

In `@apps/web/components/page-transition.tsx`:
- Around line 11-21: Remove the key state and pathname-synchronizing useEffect
from the page transition component, and pass pathname directly to the wrapper
div’s key prop. Keep the existing children and className behavior unchanged so
the wrapper remounts simultaneously with route content.

In `@apps/web/components/top-navbar.tsx`:
- Line 97: Replace the red-specific Tailwind utilities in the affected navbar
element’s className with the corresponding semantic error color tokens for text
and focus background, preserving the existing layout and dark-mode behavior.

In `@docs/enhancement-plan.md`:
- Around line 340-343: Update the enhancement plan’s implementation paths,
including the chat entries around the listed section and all repeated
references, to use apps/web/components/chat-widget.tsx instead of the nested
chat-widget path. Replace singular audit-log paths with
apps/web/app/(authenticated)/audit-logs/page.tsx, preserving the surrounding
descriptions and plan content.

In `@packages/contracts/src/audit-log/audit-log.response.ts`:
- Around line 1-27: Split the paginated schema out of audit-log.response.ts:
keep auditLogResponseSchema and AuditLogResponse there, and create
audit-log-list.response.ts containing auditLogListResponseSchema and
AuditLogListResponse. Import auditLogResponseSchema from the base response
module and update any affected exports or imports to use the new per-operation
file.

---

Outside diff comments:
In `@apps/api/src/instructors/instructors.service.ts`:
- Around line 78-93: Update the InstructorService create method to emit the
required audit log after successfully creating the instructor, using the
provided actor and created instructor details. Preserve the existing Prisma
creation and response behavior while ensuring every successful instructor
creation is recorded.
- Around line 96-124: Update the InstructorService.update method to record an
audit event using the provided actor, following the existing create
audit-logging pattern. Change the final instructor lookup to enforce tenant
scoping with gymId (using the project’s scoped findFirst convention rather than
unscoped findUniqueOrThrow), while preserving the existing response selection
and not-found behavior.

In `@docs/PROGRESS.md`:
- Around line 23-36: Synchronize the enhancement-round lifecycle status across
both tracking documents: in docs/PROGRESS.md lines 23-36, update the
overall-state sentence to indicate Features E–I are complete rather than ready
to start; in docs/enhancement-plan.md lines 3-6, replace “Not started” with the
current completed status or explicitly label the document as an implementation
history document.

---

Nitpick comments:
In `@apps/api/src/chat/chat.service.ts`:
- Line 24: Add CHAT_LLM_API_KEY and CHAT_LLM_MODEL to the relevant task’s env
configuration in turbo.json, alongside the existing environment variables, so
Turborepo recognizes both values in task cache keys.

In `@apps/api/src/checkins/checkins.service.ts`:
- Around line 127-137: Update the audit-log payloads in the check-in creation
flow, including both calls near the visible log invocation and the corresponding
later call, so entityName uses the selected member.name value instead of the
CheckIn ID template. Keep entityId unchanged.

In `@apps/web/app/`(authenticated)/audit-logs/page.tsx:
- Around line 34-87: Consolidate the duplicated getActionDetails helper shared
by the audit logs and dashboard pages into one reusable implementation,
preserving each page’s required fallback icon behavior. Correct the entityType
casing handling within the shared helper so CHECK_IN, SESSION, BOOKING, and
INSTRUCTOR matching works consistently across both callers.

In `@apps/web/app/`(authenticated)/dashboard/page.tsx:
- Around line 448-502: Update getActionDetails to handle the actual entityType
casing for CheckIn, and apply the same correction consistently in its duplicate
implementation on the audit logs page. Preserve the existing action and
entity-specific icon/color mappings.
- Around line 512-526: Replace the hand-rolled formatRelativeTime implementation
with date-fns formatDistanceToNow, reusing the existing date-fns import in this
page. Preserve the relative-time display behavior while delegating future or
clock-skewed date handling to the library, and remove the redundant local
calculation logic.
- Around line 217-271: The Live Capacity card in the dashboard uses raw
red/amber classes for full and near-capacity states instead of semantic design
tokens. Update the styling in the capacity icon, progress indicator, and status
messages associated with isFull and isNearFull to use the established error and
warning tokens, preserving the existing state-dependent appearance and layout.

In `@apps/web/app/`(authenticated)/members/[id]/page.tsx:
- Around line 49-62: Update StatusBadge to remove the external STATUS_COLORS
mapping and use the standard badge-pill classes directly, matching the
implementations in the related members and instructors pages while preserving
the existing active/inactive label behavior.

In `@apps/web/app/`(authenticated)/sessions/page.tsx:
- Around line 566-570: Update the Link element rendering session entries to
replace the Tailwind class outline-none with outline-hidden, preserving the
existing custom focus ring and other classes.

In `@apps/web/app/globals.css`:
- Around line 527-552: Replace the raw hex colors in .skeleton-shimmer,
.sidebar-gradient, and .table-premium with the appropriate existing gray and
primary design-token variables defined in globals.css, including both light and
dark theme values. Preserve each utility’s gradients, contrast, and behavior
while ensuring all listed colors use theme tokens rather than hardcoded values.

In `@apps/web/components/app-sidebar.tsx`:
- Line 238: Update the logout element’s className to replace the hardcoded red
hover background and text classes with the established error design token
classes, matching the token-based styling used by the similar logout instance in
dashboard/page.tsx while preserving the existing layout and transition classes.

In `@apps/web/components/chat-widget.tsx`:
- Around line 81-88: Add an accessible name to the close Button that calls
setIsOpen(false), using an aria-label or title describing the action as closing
the chat; leave the existing icon and styling unchanged.

In `@apps/web/hooks/use-chat.ts`:
- Around line 7-14: Update the ChatMessage model and sendMessage flow to
preserve the optional sources returned by ChatMessageResponse: add the
appropriate sources field to ChatMessage and copy response.sources onto the
assistant message instead of discarding it. Keep existing reply, loading, and
error behavior unchanged so the citations remain available for rendering.

In `@packages/contracts/src/audit-log/audit-log.response.ts`:
- Around line 4-6: Update the UUID schemas in the audit-log response definition,
including the corresponding occurrence around the referenced additional
location, from the deprecated z.string().uuid() form to top-level z.uuid().
Preserve nullable behavior for gymId and verify existing seeded/generated UUID
values comply with the stricter RFC 9562/4122 variant validation.

In `@packages/contracts/src/chat/chat-message.response.ts`:
- Around line 3-14: Move chatSourceSchema and ChatSource out of
chat-message.response.ts into a dedicated chat-source.response.ts file, then
import the schema where chatMessageResponseSchema defines sources. Update the
relevant folder index.ts and packages/contracts/src/index.ts exports so both
schemas remain publicly available.

In `@packages/database/prisma/schema.prisma`:
- Around line 314-332: Define and apply a retention or anonymization policy for
AuditLog.userName and AuditLog.ipAddress, including a mechanism to redact or
remove these values after the chosen window and during user-erasure workflows.
Keep audit event records intact while ensuring these denormalized fields are no
longer retained indefinitely.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e448b148-dfb0-466c-bcb2-dad59b50e4ea

📥 Commits

Reviewing files that changed from the base of the PR and between e7448ca and f2a767c.

⛔ Files ignored due to path filters (1)
  • apps/web/public/images/gym-hero.jpg is excluded by !**/*.jpg
📒 Files selected for processing (97)
  • apps/api/src/app.module.ts
  • apps/api/src/audit/audit.controller.ts
  • apps/api/src/audit/audit.module.ts
  • apps/api/src/audit/audit.service.ts
  • apps/api/src/audit/audit.swagger.ts
  • apps/api/src/auth/auth.module.ts
  • apps/api/src/bookings/bookings.controller.ts
  • apps/api/src/bookings/bookings.module.ts
  • apps/api/src/bookings/bookings.service.ts
  • apps/api/src/chat/chat.controller.ts
  • apps/api/src/chat/chat.module.ts
  • apps/api/src/chat/chat.service.ts
  • apps/api/src/chat/chat.swagger.ts
  • apps/api/src/checkins/checkins.controller.ts
  • apps/api/src/checkins/checkins.module.ts
  • apps/api/src/checkins/checkins.service.ts
  • apps/api/src/gyms/gyms.controller.ts
  • apps/api/src/gyms/gyms.module.ts
  • apps/api/src/gyms/gyms.service.ts
  • apps/api/src/instructors/instructors.controller.ts
  • apps/api/src/instructors/instructors.module.ts
  • apps/api/src/instructors/instructors.service.ts
  • apps/api/src/mail/mail.processor.ts
  • apps/api/src/mail/mail.service.ts
  • apps/api/src/main.ts
  • apps/api/src/me-portal/me-portal.controller.ts
  • apps/api/src/me-portal/me-portal.service.ts
  • apps/api/src/members/members.controller.ts
  • apps/api/src/members/members.module.ts
  • apps/api/src/members/members.service.ts
  • apps/api/src/plans/plans.controller.ts
  • apps/api/src/plans/plans.module.ts
  • apps/api/src/plans/plans.service.ts
  • apps/api/src/sessions/sessions.controller.ts
  • apps/api/src/sessions/sessions.module.ts
  • apps/api/src/sessions/sessions.service.ts
  • apps/api/src/subscriptions/subscriptions.controller.ts
  • apps/api/src/subscriptions/subscriptions.module.ts
  • apps/api/src/subscriptions/subscriptions.service.ts
  • apps/web/app/(authenticated)/audit-logs/page.tsx
  • apps/web/app/(authenticated)/checkins/page.tsx
  • apps/web/app/(authenticated)/dashboard/page.tsx
  • apps/web/app/(authenticated)/instructors/page.tsx
  • apps/web/app/(authenticated)/layout.tsx
  • apps/web/app/(authenticated)/members/[id]/page.tsx
  • apps/web/app/(authenticated)/members/[id]/subscriptions-panel.tsx
  • apps/web/app/(authenticated)/members/page.tsx
  • apps/web/app/(authenticated)/plans/page.tsx
  • apps/web/app/(authenticated)/sessions/[id]/page.tsx
  • apps/web/app/(authenticated)/sessions/page.tsx
  • apps/web/app/(authenticated)/settings/page.tsx
  • apps/web/app/(member)/layout.tsx
  • apps/web/app/(member)/portal/bookings/page.tsx
  • apps/web/app/(member)/portal/deactivated/page.tsx
  • apps/web/app/(member)/portal/page.tsx
  • apps/web/app/(member)/portal/plans/page.tsx
  • apps/web/app/(member)/portal/profile/page.tsx
  • apps/web/app/(member)/portal/subscriptions/page.tsx
  • apps/web/app/globals.css
  • apps/web/app/layout.tsx
  • apps/web/app/login/page.tsx
  • apps/web/components/animate-stagger.tsx
  • apps/web/components/animated-counter.tsx
  • apps/web/components/app-sidebar.tsx
  • apps/web/components/chat-widget.tsx
  • apps/web/components/dark-mode-provider.tsx
  • apps/web/components/dark-mode-toggle.tsx
  • apps/web/components/member-sidebar.tsx
  • apps/web/components/page-transition.tsx
  • apps/web/components/theme-picker.tsx
  • apps/web/components/top-navbar.tsx
  • apps/web/hooks/use-audit-logs.ts
  • apps/web/hooks/use-chat.ts
  • apps/web/hooks/use-dark-mode.ts
  • docs/PROGRESS.md
  • docs/enhancement-plan.md
  • docs/progress/PROGRESS-E.md
  • docs/progress/PROGRESS-F.md
  • docs/progress/PROGRESS-G.md
  • docs/progress/PROGRESS-H.md
  • docs/progress/PROGRESS-I.md
  • docs/test-emails.md
  • packages/contracts/src/audit-log/audit-log-list.request.ts
  • packages/contracts/src/audit-log/audit-log.response.ts
  • packages/contracts/src/audit-log/index.ts
  • packages/contracts/src/chat/chat-message.request.ts
  • packages/contracts/src/chat/chat-message.response.ts
  • packages/contracts/src/chat/index.ts
  • packages/contracts/src/index.ts
  • packages/database/prisma/migrations/20260718121133_add_audit_log/migration.sql
  • packages/database/prisma/schema.prisma
  • packages/database/prisma/seeders/seedBookings.ts
  • packages/database/prisma/seeders/seedCheckIns.ts
  • packages/database/prisma/seeders/seedGyms.ts
  • packages/database/prisma/seeders/seedMembers.ts
  • packages/database/prisma/seeders/seedSubscriptions.ts
  • packages/database/prisma/seeders/seedUsers.ts

Comment on lines +43 to +45
const gymId = user.role === 'SUPER_ADMIN' ? null : user.gymId;
return this.auditService.list(gymId, filters);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Prevent cross-tenant data exposure for unassociated ORG_ADMINs.

If an ORG_ADMIN is not associated with a gym (i.e., user.gymId is null or undefined), the gymId variable evaluates to a falsy value. When passed to this.auditService.list(gymId, filters), the downstream where-clause will omit the { gymId } filter entirely, incorrectly granting the user SUPER_ADMIN level visibility into all audit logs across all gyms.

You must explicitly reject requests from ORG_ADMINs who lack a gymId.

🔒 Proposed fix
-    const gymId = user.role === 'SUPER_ADMIN' ? null : user.gymId;
-    return this.auditService.list(gymId, filters);
+    const gymId = user.role === 'SUPER_ADMIN' ? null : user.gymId;
+    if (user.role !== 'SUPER_ADMIN' && !gymId) {
+      throw new ForbiddenException('User is not associated with a gym');
+    }
+    return this.auditService.list(gymId, filters);

(Make sure to import ForbiddenException from @nestjs/common)

📝 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 gymId = user.role === 'SUPER_ADMIN' ? null : user.gymId;
return this.auditService.list(gymId, filters);
}
const gymId = user.role === 'SUPER_ADMIN' ? null : user.gymId;
if (user.role !== 'SUPER_ADMIN' && !gymId) {
throw new ForbiddenException('User is not associated with a gym');
}
return this.auditService.list(gymId, filters);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/audit/audit.controller.ts` around lines 43 - 45, Update the
audit controller method containing the gymId assignment and auditService.list
call to import and throw ForbiddenException when an ORG_ADMIN has no associated
gymId; only call auditService.list after this validation, while preserving
SUPER_ADMIN access and existing filtering for associated users.

@@ -0,0 +1,33 @@
import { applyDecorators } from '@nestjs/common';
import { ApiOkResponse, getSchemaPath } from '@nestjs/swagger';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix static-analysis findings: unused import and any union.

  • Line 2: getSchemaPath is imported but never used.
  • Line 13: metadata!: any | null;any already subsumes null, so the union is a no-op; use unknown (or a narrower JSON type) instead.
🔧 Proposed fix
-import { applyDecorators } from '`@nestjs/common`';
-import { ApiOkResponse, getSchemaPath } from '`@nestjs/swagger`';
+import { applyDecorators } from '`@nestjs/common`';
+import { ApiOkResponse } from '`@nestjs/swagger`';
@@
-  metadata!: any | null;
+  metadata!: unknown | null;

Also applies to: 13-13

🧰 Tools
🪛 GitHub Check: check

[warning] 2-2:
'getSchemaPath' is defined but never used. Allowed unused vars must match /^_/u

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/audit/audit.swagger.ts` at line 2, Update the audit Swagger
definitions by removing the unused getSchemaPath import from the ApiOkResponse
import and changing the metadata property in the relevant response class from
any | null to unknown (or the established narrower JSON type), preserving its
nullable contract without using any.

Source: Linters/SAST tools

Comment on lines +207 to +211
async cancel(
id: string,
gymId: string,
actor: User,
): Promise<BookingResponse> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Implement missing audit logs for administrative actions.

The actor parameter was added to these service methods to support audit logging, but the this.auditService.log(...) call was omitted. This leaves the parameter unused and the action unlogged.

  • apps/api/src/bookings/bookings.service.ts#L207-L211: Add an auditService.log call for the booking.cancelled action using the actor parameter.
  • apps/api/src/gyms/gyms.service.ts#L110-L114: Add an auditService.log call for the gym.approved action using the actor parameter. (Note: You will also need to update GymsController's approve endpoint to inject @CurrentUser() user: User and pass it to the service.)
  • apps/api/src/gyms/gyms.service.ts#L161-L165: Add an auditService.log call for the gym.rejected action using the actor parameter. (Note: Update GymsController's reject endpoint to inject and pass this actor.)
  • apps/api/src/gyms/gyms.service.ts#L181-L185: Add an auditService.log call for the gym.suspended action using the actor parameter. (Note: Update GymsController's suspend endpoint to inject and pass this actor.)

The unused actor parameters were also flagged by static analysis hints.

🧰 Tools
🪛 GitHub Check: check

[warning] 210-210:
'actor' is defined but never used. Allowed unused args must match /^_/u

📍 Affects 2 files
  • apps/api/src/bookings/bookings.service.ts#L207-L211 (this comment)
  • apps/api/src/gyms/gyms.service.ts#L110-L114
  • apps/api/src/gyms/gyms.service.ts#L161-L165
  • apps/api/src/gyms/gyms.service.ts#L181-L185
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/bookings/bookings.service.ts` around lines 207 - 211, Implement
audit logging for the administrative methods: in
apps/api/src/bookings/bookings.service.ts lines 207-211, update
BookingService.cancel to log booking.cancelled with actor; in
apps/api/src/gyms/gyms.service.ts lines 110-114, 161-165, and 181-185, update
the approve, reject, and suspend methods to log gym.approved, gym.rejected, and
gym.suspended respectively using actor. Update GymsController approve, reject,
and suspend endpoints to inject `@CurrentUser`() user: User and pass it to the
corresponding service methods.

Source: Linters/SAST tools

Comment on lines +227 to +246
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: message },
],
temperature: 0.7,
max_tokens: 1000,
}),
});

if (!response.ok) {
throw new Error(`LLM API error: ${response.status}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## package.json / workspace manifests\n'
for f in package.json apps/api/package.json apps/web/package.json turbo.json .nvmrc .node-version; do
  if [ -f "$f" ]; then
    echo "--- $f"
    sed -n '1,220p' "$f"
  fi
done

printf '\n## search for AbortSignal.timeout usage\n'
rg -n "AbortSignal\.timeout|timeout\(" apps packages -g '!**/dist/**' -g '!**/build/**' || true

printf '\n## search for node engine references\n'
rg -n '"node"|engines|volta|nvm|node-version' package.json apps packages turbo.json .nvmrc .node-version -g '!**/dist/**' -g '!**/build/**' || true

Repository: chrisshaddad/bootcamp-starter

Length of output: 6378


Add a timeout to the LLM fetch call.

fetch here can hang indefinitely; pass signal: AbortSignal.timeout(15000) so the request fails fast and falls back to the rule-based response.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/chat/chat.service.ts` around lines 227 - 246, Update the fetch
call in the chat service to include an AbortSignal.timeout(15000) signal option,
ensuring the LLM request fails after 15 seconds and reaches the existing
rule-based fallback handling.

to: email,
from: 'no-reply@bootcamp-starter.local',
from: 'no-reply@gymflow.io',
subject: 'Sign in to Bootcamp Starter',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Incomplete rebranding in email templates.

The sender email address was successfully updated to gymflow.io across the processor, but the email subject and body templates still reference the legacy "Bootcamp Starter" brand name.

  • apps/api/src/mail/mail.processor.ts#L66-L66: update the subject string to "Sign in to GymFlow".
  • apps/api/src/mail/mail.processor.ts#L105-L105: update the body text to say "has invited you to join ${organizationName} on GymFlow."
📍 Affects 1 file
  • apps/api/src/mail/mail.processor.ts#L66-L66 (this comment)
  • apps/api/src/mail/mail.processor.ts#L105-L105
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/mail/mail.processor.ts` at line 66, Update the email templates
in mail.processor.ts: change the subject to “Sign in to GymFlow” at lines 66-66
and update the body text to “has invited you to join ${organizationName} on
GymFlow.” at lines 105-105.

Comment on lines +24 to +29
function formatChatMarkdown(text: string): string {
return text
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/^- /gm, '• ')
.replace(/\n/g, '<br />');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and inspect the surrounding lines
ast-grep outline apps/web/components/chat-widget.tsx --view expanded || true
echo '---'
sed -n '1,220p' apps/web/components/chat-widget.tsx | cat -n

echo '--- chat service search ---'
rg -n "sendMessage|callLLM|dangerouslySetInnerHTML|aria-label|title=" apps/web -S

Repository: chrisshaddad/bootcamp-starter

Length of output: 10041


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Narrowly inspect the chat service implementation if present
fd -a "ChatService" apps/web . || true
rg -n "class ChatService|function sendMessage|const sendMessage|callLLM" apps/web -S

Repository: chrisshaddad/bootcamp-starter

Length of output: 260


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,220p' apps/web/hooks/use-chat.ts | cat -n

Repository: chrisshaddad/bootcamp-starter

Length of output: 3322


Escape assistant output before dangerouslySetInnerHTML.

formatChatMarkdown leaves HTML unescaped, so any <...> in msg.content is rendered as real markup at apps/web/components/chat-widget.tsx:121-126. Sanitize or escape before injection. The close button at 81-88 also needs an accessible label (aria-label or title).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/components/chat-widget.tsx` around lines 24 - 29, Update
formatChatMarkdown to escape HTML-sensitive characters in assistant text before
applying markdown formatting, ensuring msg.content cannot inject markup through
dangerouslySetInnerHTML. Also add an accessible aria-label or title to the close
button near the chat widget controls.

Comment on lines +11 to +21
const pathname = usePathname();
const [key, setKey] = useState(pathname);

useEffect(() => {
setKey(pathname);
}, [pathname]);

return (
<div key={key} className="page-transition">
{children}
</div>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Avoid using useEffect to sync the key, as it causes a double mount.

When pathname changes, the children prop immediately updates to the new route's content. However, because key is managed in a useEffect, the new page components briefly mount inside the wrapper using the old key. The effect then fires, sets the new key, and forces React to completely destroy the wrapper and remount all new page components a second time. This causes wasted renders and executes all page-level useEffect hooks twice.

Pass pathname directly to the key prop so the wrapper and its new content remount simultaneously in one render pass.

⚡ Proposed fix for the double render
 export function PageTransition({ children }: { children: ReactNode }) {
   const pathname = usePathname();
-  const [key, setKey] = useState(pathname);
-
-  useEffect(() => {
-    setKey(pathname);
-  }, [pathname]);
 
   return (
-    <div key={key} className="page-transition">
+    <div key={pathname} className="page-transition">
       {children}
     </div>
   );
 }
📝 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 pathname = usePathname();
const [key, setKey] = useState(pathname);
useEffect(() => {
setKey(pathname);
}, [pathname]);
return (
<div key={key} className="page-transition">
{children}
</div>
const pathname = usePathname();
return (
<div key={pathname} className="page-transition">
{children}
</div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/components/page-transition.tsx` around lines 11 - 21, Remove the key
state and pathname-synchronizing useEffect from the page transition component,
and pass pathname directly to the wrapper div’s key prop. Keep the existing
children and className behavior unchanged so the wrapper remounts simultaneously
with route content.

<DropdownMenuItem
onClick={() => logout()}
className="flex items-center gap-2 text-red-600 focus:bg-red-50 focus:text-red-600"
className="flex items-center gap-2 text-red-600 dark:text-red-400 focus:bg-red-50 dark:focus:bg-red-900/20 focus:text-red-600 dark:focus:text-red-400"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace arbitrary red color utilities with the semantic error token.

As per coding guidelines, use semantic design tokens like error instead of arbitrary colors (e.g., red-600, red-900/20) to ensure consistency and proper dark mode support across the application.

🎨 Proposed fix
-              className="flex items-center gap-2 text-red-600 dark:text-red-400 focus:bg-red-50 dark:focus:bg-red-900/20 focus:text-red-600 dark:focus:text-red-400"
+              className="flex items-center gap-2 text-error focus:bg-error/10 focus:text-error"
📝 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="flex items-center gap-2 text-red-600 dark:text-red-400 focus:bg-red-50 dark:focus:bg-red-900/20 focus:text-red-600 dark:focus:text-red-400"
className="flex items-center gap-2 text-error focus:bg-error/10 focus:text-error"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/components/top-navbar.tsx` at line 97, Replace the red-specific
Tailwind utilities in the affected navbar element’s className with the
corresponding semantic error color tokens for text and focus background,
preserving the existing layout and dark-mode behavior.

Source: Coding guidelines

Comment thread docs/enhancement-plan.md
Comment on lines +340 to +343
- `apps/web/components/chat/chat-widget.tsx` — floating button + panel
- `apps/web/components/chat/chat-message.tsx` — individual message bubble
- `apps/web/components/chat/chat-input.tsx` — input area
- `apps/web/hooks/use-chat.ts` — SWR mutation hook for sending messages

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct stale implementation paths.

The plan references apps/web/components/chat/chat-widget.tsx and singular audit-log paths, but the supplied implementation context uses apps/web/components/chat-widget.tsx and apps/web/app/(authenticated)/audit-logs/page.tsx. Update these references to prevent future work from targeting nonexistent paths.

Also applies to: 471-472, 609-609

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/enhancement-plan.md` around lines 340 - 343, Update the enhancement
plan’s implementation paths, including the chat entries around the listed
section and all repeated references, to use apps/web/components/chat-widget.tsx
instead of the nested chat-widget path. Replace singular audit-log paths with
apps/web/app/(authenticated)/audit-logs/page.tsx, preserving the surrounding
descriptions and plan content.

Comment on lines +1 to +27
import { z } from 'zod';

export const auditLogResponseSchema = z.object({
id: z.string().uuid(),
gymId: z.string().uuid().nullable(),
userId: z.string().uuid(),
userName: z.string(),
action: z.string(),
entityType: z.string(),
entityId: z.string().uuid(),
entityName: z.string().nullable(),
metadata: z.any().nullable(),
ipAddress: z.string().nullable(),
createdAt: z.coerce.date(),
});

export type AuditLogResponse = z.infer<typeof auditLogResponseSchema>;

export const auditLogListResponseSchema = z.object({
data: z.array(auditLogResponseSchema),
total: z.number(),
page: z.number(),
limit: z.number(),
totalPages: z.number(),
});

export type AuditLogListResponse = z.infer<typeof auditLogListResponseSchema>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Split into per-operation files per contracts convention.

This file bundles the base item schema (auditLogResponseSchema) and the paginated list schema (auditLogListResponseSchema) together, and the list schema doesn't use the -list suffix used by the sibling audit-log-list.request.ts. As per coding guidelines, "Create one schema per file in packages/contracts. Name files as <resource>-<operation>.{request,response}.ts."

📁 Proposed split

audit-log.response.ts (keep only the base schema):

 import { z } from 'zod';

 export const auditLogResponseSchema = z.object({ ... });
 export type AuditLogResponse = z.infer<typeof auditLogResponseSchema>;
-
-export const auditLogListResponseSchema = z.object({ ... });
-export type AuditLogListResponse = z.infer<typeof auditLogListResponseSchema>;

New audit-log-list.response.ts:

import { z } from 'zod';
import { auditLogResponseSchema } from './audit-log.response';

export const auditLogListResponseSchema = z.object({
  data: z.array(auditLogResponseSchema),
  total: z.number(),
  page: z.number(),
  limit: z.number(),
  totalPages: z.number(),
});

export type AuditLogListResponse = z.infer<typeof auditLogListResponseSchema>;
📝 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
import { z } from 'zod';
export const auditLogResponseSchema = z.object({
id: z.string().uuid(),
gymId: z.string().uuid().nullable(),
userId: z.string().uuid(),
userName: z.string(),
action: z.string(),
entityType: z.string(),
entityId: z.string().uuid(),
entityName: z.string().nullable(),
metadata: z.any().nullable(),
ipAddress: z.string().nullable(),
createdAt: z.coerce.date(),
});
export type AuditLogResponse = z.infer<typeof auditLogResponseSchema>;
export const auditLogListResponseSchema = z.object({
data: z.array(auditLogResponseSchema),
total: z.number(),
page: z.number(),
limit: z.number(),
totalPages: z.number(),
});
export type AuditLogListResponse = z.infer<typeof auditLogListResponseSchema>;
import { z } from 'zod';
export const auditLogResponseSchema = z.object({
id: z.string().uuid(),
gymId: z.string().uuid().nullable(),
userId: z.string().uuid(),
userName: z.string(),
action: z.string(),
entityType: z.string(),
entityId: z.string().uuid(),
entityName: z.string().nullable(),
metadata: z.any().nullable(),
ipAddress: z.string().nullable(),
createdAt: z.coerce.date(),
});
export type AuditLogResponse = z.infer<typeof auditLogResponseSchema>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/contracts/src/audit-log/audit-log.response.ts` around lines 1 - 27,
Split the paginated schema out of audit-log.response.ts: keep
auditLogResponseSchema and AuditLogResponse there, and create
audit-log-list.response.ts containing auditLogListResponseSchema and
AuditLogListResponse. Import auditLogResponseSchema from the base response
module and update any affected exports or imports to use the new per-operation
file.

Source: Coding guidelines

@khaled7172 khaled7172 closed this Jul 20, 2026
@khaled7172
khaled7172 deleted the gym-enhancements-contrast-chat branch July 20, 2026 21:18
@khaled7172

Copy link
Copy Markdown
Collaborator Author

Mohammad used my changes and added his own UI on top of them, so i closed this pull request

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant