Gym enhancements contrast chat - #178
Conversation
…ardize member/session details
…tyCard icons/layout, and polish audit log counter
…h keepPreviousData
📝 WalkthroughWalkthroughThis 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. ChangesAudit logging
AI chat
Web experience and branding
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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
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. Comment |
There was a problem hiding this comment.
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 winSynchronize 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 winAdd missing audit logging for instructor creation.
The
actorparameter 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 winAdd missing audit logging and enforce tenant scoping.
Similar to
create, theactorparameter 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 "UsefindFirst({ where: { id, organizationId } })even forfindUniquecalls" 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 winDeclare
CHAT_LLM_API_KEYandCHAT_LLM_MODELinturbo.json.Static analysis flags both env vars as undeclared. Add them to the relevant task's
envinturbo.jsonso 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 winSplit
chatSourceSchemainto its own file.This file defines two exported schemas. As per coding guidelines, "Create one schema per file in
packages/contracts". MovechatSourceSchema/ChatSourceinto a dedicated file (e.g.chat-source.response.ts) and import it here, updating the folderindex.tsandpackages/contracts/src/index.tsaccordingly.🤖 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 winClose button lacks an accessible name.
Unlike the FAB (
aria-label="Toggle chat") and clear button (title="Clear chat"), the close (X) icon button has notitle/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
sourcesfrom the chat response is never surfaced to the user.
ChatMessageResponse(packages/contracts/src/chat/chat-message.response.ts) optionally returns asourcesarray for grounding citations, butChatMessagehas no field for it andsendMessageonly copiesresponse.replyinto state —response.sourcesis 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 winPrefer design tokens over raw hex in
skeleton-shimmer,sidebar-gradient, andtable-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 usesvar(--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 inglobals.cssinstead 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 winStandardize the
StatusBadgeimplementation.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.tsxandinstructors/page.tsx), use the standardbadge-pillclasses 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 winSwap
outline-noneforoutline-hidden. Tailwind v4 treatsoutline-noneas a strict outline removal;outline-hiddenpreserves 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
getActionDetailsduplicated fromaudit-logs/page.tsx— shares its entityType-casing bug.This is a verbatim copy of
getActionDetailsfromapps/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 winHand-rolled relative-time formatting duplicates
date-fns.
formatRelativeTimereimplements whatdate-fns'sformatDistanceToNowalready does (and is already used for the same purpose inaudit-logs/page.tsx, withdate-fnsimported elsewhere in this very file fordifferenceInDays). The hand-rolled version also has a subtle edge case: a future/clock-skeweddateproduces a negativediffInMinutes, which still satisfiesdiffInMinutes < 1and 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 winFull-capacity/near-capacity styling uses raw red/amber instead of semantic tokens.
Line 225 (
text-red-500icon) and line 262 (text-red-600 dark:text-red-400"Gym is full!" text) hardcode red rather than using theerrordesign token the coding guidelines call out for this exact purpose. Flagging for consolidation with a similar instance inapp-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 winLogout hover uses raw red instead of the
errordesign token.As per coding guidelines, "Apply design tokens (
primary-base,primary-100,gray-*,error) defined inglobals.cssinstead of arbitrary color values." This hover state hardcodesred-50/red-600/red-900rather than theerrortoken. Flagging for consolidation with a similar instance indashboard/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 winUse the member's name for
entityNameinstead of repeating the ID.Both audit-log calls set
entityName: \CheckIn ${id}`, which just repeats the entity's own ID. TheCHECKIN_SELECTprojection already includesmember.name, so the audit-log UI (which rendersentityName` 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 | 🔵 TrivialConsider a retention/anonymization policy for
ipAddressanduserName.
AuditLogdenormalizesuserNameand stores rawipAddresswith no expiry, and (unlikeSession/MagicLink) it lives in thepublicschema. Since this data persists indefinitely and isn't tied to the sourceUserrecord, 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 winModernize
.uuid()chain to top-levelz.uuid()(Zod 4).
z.string().uuid()still works in Zod 4.3.5 but is deprecated in favor of the top-levelz.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
getActionDetailsis duplicated verbatim indashboard/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
⛔ Files ignored due to path filters (1)
apps/web/public/images/gym-hero.jpgis excluded by!**/*.jpg
📒 Files selected for processing (97)
apps/api/src/app.module.tsapps/api/src/audit/audit.controller.tsapps/api/src/audit/audit.module.tsapps/api/src/audit/audit.service.tsapps/api/src/audit/audit.swagger.tsapps/api/src/auth/auth.module.tsapps/api/src/bookings/bookings.controller.tsapps/api/src/bookings/bookings.module.tsapps/api/src/bookings/bookings.service.tsapps/api/src/chat/chat.controller.tsapps/api/src/chat/chat.module.tsapps/api/src/chat/chat.service.tsapps/api/src/chat/chat.swagger.tsapps/api/src/checkins/checkins.controller.tsapps/api/src/checkins/checkins.module.tsapps/api/src/checkins/checkins.service.tsapps/api/src/gyms/gyms.controller.tsapps/api/src/gyms/gyms.module.tsapps/api/src/gyms/gyms.service.tsapps/api/src/instructors/instructors.controller.tsapps/api/src/instructors/instructors.module.tsapps/api/src/instructors/instructors.service.tsapps/api/src/mail/mail.processor.tsapps/api/src/mail/mail.service.tsapps/api/src/main.tsapps/api/src/me-portal/me-portal.controller.tsapps/api/src/me-portal/me-portal.service.tsapps/api/src/members/members.controller.tsapps/api/src/members/members.module.tsapps/api/src/members/members.service.tsapps/api/src/plans/plans.controller.tsapps/api/src/plans/plans.module.tsapps/api/src/plans/plans.service.tsapps/api/src/sessions/sessions.controller.tsapps/api/src/sessions/sessions.module.tsapps/api/src/sessions/sessions.service.tsapps/api/src/subscriptions/subscriptions.controller.tsapps/api/src/subscriptions/subscriptions.module.tsapps/api/src/subscriptions/subscriptions.service.tsapps/web/app/(authenticated)/audit-logs/page.tsxapps/web/app/(authenticated)/checkins/page.tsxapps/web/app/(authenticated)/dashboard/page.tsxapps/web/app/(authenticated)/instructors/page.tsxapps/web/app/(authenticated)/layout.tsxapps/web/app/(authenticated)/members/[id]/page.tsxapps/web/app/(authenticated)/members/[id]/subscriptions-panel.tsxapps/web/app/(authenticated)/members/page.tsxapps/web/app/(authenticated)/plans/page.tsxapps/web/app/(authenticated)/sessions/[id]/page.tsxapps/web/app/(authenticated)/sessions/page.tsxapps/web/app/(authenticated)/settings/page.tsxapps/web/app/(member)/layout.tsxapps/web/app/(member)/portal/bookings/page.tsxapps/web/app/(member)/portal/deactivated/page.tsxapps/web/app/(member)/portal/page.tsxapps/web/app/(member)/portal/plans/page.tsxapps/web/app/(member)/portal/profile/page.tsxapps/web/app/(member)/portal/subscriptions/page.tsxapps/web/app/globals.cssapps/web/app/layout.tsxapps/web/app/login/page.tsxapps/web/components/animate-stagger.tsxapps/web/components/animated-counter.tsxapps/web/components/app-sidebar.tsxapps/web/components/chat-widget.tsxapps/web/components/dark-mode-provider.tsxapps/web/components/dark-mode-toggle.tsxapps/web/components/member-sidebar.tsxapps/web/components/page-transition.tsxapps/web/components/theme-picker.tsxapps/web/components/top-navbar.tsxapps/web/hooks/use-audit-logs.tsapps/web/hooks/use-chat.tsapps/web/hooks/use-dark-mode.tsdocs/PROGRESS.mddocs/enhancement-plan.mddocs/progress/PROGRESS-E.mddocs/progress/PROGRESS-F.mddocs/progress/PROGRESS-G.mddocs/progress/PROGRESS-H.mddocs/progress/PROGRESS-I.mddocs/test-emails.mdpackages/contracts/src/audit-log/audit-log-list.request.tspackages/contracts/src/audit-log/audit-log.response.tspackages/contracts/src/audit-log/index.tspackages/contracts/src/chat/chat-message.request.tspackages/contracts/src/chat/chat-message.response.tspackages/contracts/src/chat/index.tspackages/contracts/src/index.tspackages/database/prisma/migrations/20260718121133_add_audit_log/migration.sqlpackages/database/prisma/schema.prismapackages/database/prisma/seeders/seedBookings.tspackages/database/prisma/seeders/seedCheckIns.tspackages/database/prisma/seeders/seedGyms.tspackages/database/prisma/seeders/seedMembers.tspackages/database/prisma/seeders/seedSubscriptions.tspackages/database/prisma/seeders/seedUsers.ts
| const gymId = user.role === 'SUPER_ADMIN' ? null : user.gymId; | ||
| return this.auditService.list(gymId, filters); | ||
| } |
There was a problem hiding this comment.
🔒 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.
| 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'; | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Fix static-analysis findings: unused import and any union.
- Line 2:
getSchemaPathis imported but never used. - Line 13:
metadata!: any | null;—anyalready subsumesnull, so the union is a no-op; useunknown(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
| async cancel( | ||
| id: string, | ||
| gymId: string, | ||
| actor: User, | ||
| ): Promise<BookingResponse> { |
There was a problem hiding this comment.
🎯 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 anauditService.logcall for thebooking.cancelledaction using theactorparameter.apps/api/src/gyms/gyms.service.ts#L110-L114: Add anauditService.logcall for thegym.approvedaction using theactorparameter. (Note: You will also need to updateGymsController'sapproveendpoint to inject@CurrentUser() user: Userand pass it to the service.)apps/api/src/gyms/gyms.service.ts#L161-L165: Add anauditService.logcall for thegym.rejectedaction using theactorparameter. (Note: UpdateGymsController'srejectendpoint to inject and pass this actor.)apps/api/src/gyms/gyms.service.ts#L181-L185: Add anauditService.logcall for thegym.suspendedaction using theactorparameter. (Note: UpdateGymsController'ssuspendendpoint 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-L114apps/api/src/gyms/gyms.service.ts#L161-L165apps/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
| 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}`); | ||
| } |
There was a problem hiding this comment.
🩺 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/**' || trueRepository: 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', |
There was a problem hiding this comment.
📐 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.
| function formatChatMarkdown(text: string): string { | ||
| return text | ||
| .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') | ||
| .replace(/^- /gm, '• ') | ||
| .replace(/\n/g, '<br />'); | ||
| } |
There was a problem hiding this comment.
🔒 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 -SRepository: 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 -SRepository: 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 -nRepository: 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.
| const pathname = usePathname(); | ||
| const [key, setKey] = useState(pathname); | ||
|
|
||
| useEffect(() => { | ||
| setKey(pathname); | ||
| }, [pathname]); | ||
|
|
||
| return ( | ||
| <div key={key} className="page-transition"> | ||
| {children} | ||
| </div> |
There was a problem hiding this comment.
🎯 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.
| 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" |
There was a problem hiding this comment.
📐 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.
| 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
| - `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 |
There was a problem hiding this comment.
📐 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.
| 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>; |
There was a problem hiding this comment.
🗄️ 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.
| 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
|
Mohammad used my changes and added his own UI on top of them, so i closed this pull request |
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
• 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.
• 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
Summary by CodeRabbit
New Features
Enhancements
Bug Fixes