Skip to content

Conversation

@ucswift
Copy link
Member

@ucswift ucswift commented Feb 9, 2026

Summary by CodeRabbit

  • New Features

    • Broader web support: call handling, background/location updates, and push/notification fallbacks for web.
  • Improvements

    • Faster, more stable UI: memoization, reduced re-renders, loading overlay during init, sidebar UI, and map performance/interaction refinements.
    • Audio & permissions: improved device selection and permission flows with platform-aware behavior.
    • Security & assets: relaxed content policy and safer production asset loading for desktop builds.
  • Configuration

    • Added runtime environment variables and build-time production defaults.

@coderabbitai
Copy link

coderabbitai bot commented Feb 9, 2026

Warning

Rate limit exceeded

@ucswift has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 25 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Converted many Zustand store consumers to selector-based access, added extensive web shims and platform guards for native services, memoized UI context objects, refactored map/web rendering and Electron asset loading, updated build/runtime environment vars, and migrated tests to selector-aware mocks.

Changes

Cohort / File(s) Summary
Store & Hook refactor
src/stores/..., src/hooks/..., src/lib/auth/index.tsx
Replaced broad destructures with per-field selectors across many stores/hooks (granular subscriptions, no public API surface break except documented signature changes).
Web shims & native-guarded services
src/lib/countly-*.web.ts, src/lib/empty-module.web.js, src/lib/native-module-shims.web.ts, src/services/{bluetooth-audio,callkeep,location,push-notification,notification-sound,callkeep.service.web}.ts
Added web no-op shims and Platform.OS === 'web' guards; new web CallKeep implementation and Countly/CountlyConfig shims; many service methods become no-ops or return defaults on web.
Map & marker web refactor
src/app/(app)/index.tsx, src/components/maps/{map-view.web.tsx,map-pins.tsx,pin-marker.tsx,pin-detail-modal.tsx}
Deferred heavy map init until core ready, split location into lat/long/heading selectors, replaced JS animations with CSS on web, persistent React roots for marker children, and memoized per-pin components.
UI context memoization
src/components/ui/* (button,input,checkbox,modal,drawer,popover,skeleton, etc.)
Memoized context objects passed to underlying UI primitives via useMemo to stabilize references and reduce unnecessary re-renders.
Electron, Docker & runtime env
electron/main.js, Dockerfile, docker-compose.yml, docker/docker-entrypoint.sh, nginx.conf
Registered privileged app:// protocol for production, changed web build env to APP_ENV=production, added UNIT_BUNDLE_ID/UNIT_PACKAGE env vars to build/run config, and broadened CSP in nginx.
Build / tooling config
babel.config.js, .eslintrc.js, package.json, metro.config.js
Added babel-plugin-transform-import-meta, removed Tailwind ESLint plugin/config, updated lint script, added Metro web resolution shims for Countly/Zustand and blocked server-only packages via an empty-module shim.
Layout & initialization
src/app/(app)/_layout.tsx, src/app/_layout.tsx
Deferred UI until init complete, added isInitComplete gating, debounced analytics, Novu provider stable mounting, adjusted Sentry/native-frame config and platform-gated badge clearing.
SignalR / location / stores with persistence & guards
src/services/signalr.service.ts, src/services/location.ts, src/stores/* (core-store, livekit-store, loading-store, calls, roles, security, location-store)
Pre-connection listener cleanup, platform-aware logging, location background/watch shims for web (navigator.geolocation), added isInitialized guards, partialize persist configs, LiveKit requestPermissions signature changed to return boolean, and toggleLoading added.
Tests & mocks
jest-setup.ts, __mocks__/react-native-svg.ts, many src/**/__tests__/*.test.tsx
Introduced Platform mock in tests, comprehensive react-native-svg mock, migrated test mocks from mockReturnValue to mockImplementation supporting selector functions (updated many test files to mirror selector-based stores).
Patches & scripts
scripts/patch-nativewind.py
New postinstall patch script to modify react-native-css-interop internals (dark-mode trimming, observer guards, cleanup, React.createElement changes).

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

🐰 A hop toward web, I tweak each store and thread,
Memoized my contexts so re-renders rest their head,
CallKeep hums in browsers, Countly gently sleeps,
Tests now speak in selectors while the CI softly peeps,
I nibble env vars, build with care—this little rabbit leaps!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% 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 'Develop' is a vague branch name rather than a descriptive summary of the changeset. It does not convey what was actually changed or improved. Provide a clear, specific title summarizing the main change. For example: 'Add web platform support and refactor store selectors' or 'Support web platform with Zustand selector pattern updates'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

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

⚠️ Outside diff range comments (15)
src/components/ui/shared-tabs.tsx (1)

17-20: ⚠️ Potential issue | 🟠 Major

Global singleton store will cause conflicts between multiple SharedTabs instances.

useTabStore is created once at module scope, so every SharedTabs that receives an onChange prop shares the same activeIndex. If two tab groups coexist (e.g., on the same screen or across mounted screens), selecting a tab in one will switch the other.

Additionally, initialIndex (line 57) only seeds localActiveIndex — the global store always starts at 0, so initialIndex is silently ignored when onChange is provided.

Consider either:

  1. Lifting tab state entirely to the parent (controlled component pattern — remove the store, accept activeIndex + onChange as props), or
  2. Using local useState exclusively (the store adds no value here since it isn't consumed outside this component).

Option 1 is the cleanest for a shared UI component:

Proposed refactor (controlled + uncontrolled pattern)
-// Tab state management with zustand
-interface TabState {
-  activeIndex: number;
-  setActiveIndex: (index: number) => void;
-}
-
-const useTabStore = create<TabState>((set) => ({
-  activeIndex: 0,
-  setActiveIndex: (index) => set({ activeIndex: index }),
-}));
 interface SharedTabsProps {
   tabs: TabItem[];
+  activeIndex?: number;        // controlled mode
   initialIndex?: number;
   scrollable?: boolean;
   ...
 }
-  const [localActiveIndex, setLocalActiveIndex] = useState(initialIndex);
-  const activeIndex = useTabStore((s) => s.activeIndex);
-  const setActiveIndex = useTabStore((s) => s.setActiveIndex);
+  const [localActiveIndex, setLocalActiveIndex] = useState(initialIndex);
+  const currentIndex = activeIndex ?? localActiveIndex;

Also applies to: 58-59

src/services/push-notification.ts (1)

465-472: ⚠️ Potential issue | 🟡 Minor

Push token logged in plaintext.

The FCM push token is logged at info level. While useful for debugging, push tokens are semi-sensitive — they can be used to send notifications to a specific device. Consider logging only a truncated portion or moving this to debug level so it's excluded from production logs (where severity is 'warn').

src/components/settings/audio-device-selection.tsx (1)

47-47: ⚠️ Potential issue | 🟡 Minor

Hardcoded English string bypasses i18n.

The fallback ' Device' suffix is not wrapped in t(), so it won't be translated. As per coding guidelines, all user-facing text should use t() from react-i18next.

Proposed fix
-      return deviceType.charAt(0).toUpperCase() + deviceType.slice(1) + ' Device';
+      return t('settings.audio_device_selection.generic_device', { type: deviceType.charAt(0).toUpperCase() + deviceType.slice(1) });
src/components/settings/bluetooth-device-selection-bottom-sheet.tsx (1)

258-283: ⚠️ Potential issue | 🟡 Minor

Hardcoded user-facing strings should be wrapped in t().

Lines 258, 281, and 283 contain hardcoded English text ("System Audio", "AirPods, Car, Wired Headset") that should use t() for internationalization. As per coding guidelines, all user-facing text must be wrapped in t() from react-i18next.

♻️ Suggested fix
-                await setPreferredDevice({ id: 'system-audio', name: 'System Audio' });
+                await setPreferredDevice({ id: 'system-audio', name: t('bluetooth.system_audio') });
-                  <Text className={`font-medium ${preferredDevice?.id === 'system-audio' ? 'text-primary-700 dark:text-primary-300' : 'text-neutral-900 dark:text-neutral-100'}`}>
-                    System Audio
-                  </Text>
-                  <Text className="text-xs text-neutral-500">AirPods, Car, Wired Headset</Text>
+                  <Text className={`font-medium ${preferredDevice?.id === 'system-audio' ? 'text-primary-700 dark:text-primary-300' : 'text-neutral-900 dark:text-neutral-100'}`}>
+                    {t('bluetooth.system_audio')}
+                  </Text>
+                  <Text className="text-xs text-neutral-500">{t('bluetooth.system_audio_description')}</Text>
src/components/roles/roles-modal.tsx (1)

60-80: ⚠️ Potential issue | 🟠 Major

Bug: Unassignment logic is broken — || operator falls back to the current assignment.

On line 69, pendingAssignment?.userId || currentAssignment?.UserId || '' uses ||, so when a pending unassignment sets userId to undefined (line 49), the expression falls through to currentAssignment?.UserId, effectively ignoring the unassignment.

Compare with roles-bottom-sheet.tsx which handles this differently by excluding entries without a userId from pendingAssignments entirely (lines 67–71) and filtering out roles without UserId in the save (line 100).

To fix, use nullish coalescing (??) or check for the existence of a pending entry separately:

Proposed fix
-          const assignedUserId = pendingAssignment?.userId || currentAssignment?.UserId || '';
+          const assignedUserId = pendingAssignment
+            ? (pendingAssignment.userId ?? '')
+            : (currentAssignment?.UserId ?? '');
src/stores/calls/store.ts (1)

32-48: ⚠️ Potential issue | 🔴 Critical

The new isLoading guard combined with missing error handling permanently locks the store on failure.

The init method has no try/catch. If any of the three API calls (lines 38–40) fails, isLoading remains true and isInitialized remains false. The new guard at line 34 checks get().isLoading, so any subsequent init() call will return early — the store is permanently stuck in a loading state with no way to recover.

Before this PR, a failed init would still leave isLoading: true, but re-mounting could re-enter init. The new guard makes this a hard lock.

Proposed fix: wrap in try/catch like the roles store
   init: async () => {
     // Prevent re-initialization during tree remounts
     if (get().isInitialized || get().isLoading) {
       return;
     }
     set({ isLoading: true, error: null });
+    try {
       const callsResponse = await getCalls();
       const callPrioritiesResponse = await getCallPriorities();
       const callTypesResponse = await getCallTypes();
       set({
         calls: Array.isArray(callsResponse.Data) ? callsResponse.Data : [],
         callPriorities: Array.isArray(callPrioritiesResponse.Data) ? callPrioritiesResponse.Data : [],
         callTypes: Array.isArray(callTypesResponse.Data) ? callTypesResponse.Data : [],
         isLoading: false,
         isInitialized: true,
         lastFetchedAt: Date.now(),
       });
+    } catch (error) {
+      set({ error: 'Failed to initialize calls', isLoading: false });
+    }
   },
src/app/call/new/index.tsx (2)

2-2: ⚠️ Potential issue | 🟠 Major

Remove unused @testing-library/react-native import from production code.

render is imported but never used in this file. Including a test-only library in a production component inflates the bundle and may cause issues on platforms where the library isn't available.

Proposed fix
-import { render } from '@testing-library/react-native';

204-204: ⚠️ Potential issue | 🟡 Minor

Remove console.log that dumps the full form data in production.

This logs the entire call creation payload including address, coordinates, contact info, and dispatch details. Consider removing it or gating behind __DEV__.

Proposed fix
-      console.log('Creating new call with data:', data);
src/components/calls/call-images-modal.tsx (1)

426-426: ⚠️ Potential issue | 🟠 Major

Math.random() in keyExtractor breaks React's reconciliation.

When an item lacks an Id, the random fallback produces a new key every render, causing the FlatList to remount items and lose scroll position. Use the index parameter or a stable fallback instead.

Proposed fix
-            keyExtractor={(item) => item?.Id || `image-${Math.random()}`}
+            keyExtractor={(item, index) => item?.Id || `image-fallback-${index}`}
src/app/call/[id]/edit.tsx (1)

215-215: ⚠️ Potential issue | 🟡 Minor

Remove console.log before merging.

Line 215 has a console.log that should be replaced with logger.debug (or removed) to stay consistent with the structured logging used elsewhere in the codebase.

Proposed fix
-      console.log('Updating call with data:', data);
+      logger.debug({ message: 'Updating call with data', context: { data } });
src/services/bluetooth-audio.service.ts (1)

852-853: ⚠️ Potential issue | 🟡 Minor

Duplicate assignment: this.isInitialized = false appears twice.

Line 852 and 853 are identical. One should likely be removed — or the second was intended to reset a different flag.

🐛 Proposed fix
     // Reset initialization flags
     this.isInitialized = false;
-    this.isInitialized = false;
     this.hasAttemptedPreferredDeviceConnection = false;
src/components/maps/map-view.web.tsx (1)

286-293: ⚠️ Potential issue | 🟠 Major

getBoundingClientRect() on an empty container will return zero dimensions.

When anchor is an {x, y} object, this code calls container.getBoundingClientRect() immediately after creating the marker. At this point, the container has no children rendered yet (children are rendered in a separate effect at Lines 325-329), so rect.width and rect.height will both be 0, making the offset calculation always [0, 0].

Consider moving the offset calculation into the children-rendering effect, after root.render(...) completes, or deferring it with requestAnimationFrame.

🐛 Sketch of a fix — defer offset calculation
+  // Recalculate anchor offset after children are rendered
+  useEffect(() => {
+    if (
+      markerRef.current &&
+      containerRef.current &&
+      typeof anchor === 'object' && anchor !== null && 'x' in anchor && 'y' in anchor
+    ) {
+      // Use rAF to ensure layout has settled after React render
+      const frameId = requestAnimationFrame(() => {
+        const rect = containerRef.current?.getBoundingClientRect();
+        if (rect && rect.width > 0 && rect.height > 0) {
+          const xOffset = (anchor.x - 0.5) * rect.width;
+          const yOffset = (anchor.y - 0.5) * rect.height;
+          markerRef.current?.setOffset([xOffset, yOffset]);
+        }
+      });
+      return () => cancelAnimationFrame(frameId);
+    }
+  }, [children, anchor]);

And remove the inline offset calculation from Lines 288-293.

src/features/livekit-call/components/LiveKitCallModal.tsx (1)

88-152: ⚠️ Potential issue | 🟡 Minor

Hardcoded English strings should be wrapped in t() for i18n.

Multiple user-facing strings are not wrapped in t(): "Connecting to", "Connection Error", "Try Again", "Connected", "You are in room:", "Your ID:", "Mute", "Unmute", "Leave Call", "Join a Voice Call", "Select a room to join:", and the "Join" button text. As per coding guidelines, all user-facing text must use t() from react-i18next with dictionary files in src/translations.

src/app/(app)/_layout.tsx (1)

104-160: ⚠️ Potential issue | 🟡 Minor

Initialization failure dismisses loading overlay without user feedback.

setIsInitComplete(true) in the finally block (line 158) means the loading overlay is removed even when initialization fails. While hasInitialized.current stays false allowing retry, the user sees the full app shell without any indication that data loading failed or a way to retry.

Consider showing an error state or retry prompt instead of silently proceeding.

src/stores/app/livekit-store.ts (1)

558-601: ⚠️ Potential issue | 🟡 Minor

Fix disconnectFromRoom return type: interface declares void but implementation is async.

The interface at line 161 declares disconnectFromRoom: () => void, but the implementation at line 558 is async, returning a Promise. This creates a type contract violation where callers like src/services/app-reset.service.ts:215 correctly await the function, while UI components in LiveKitCallModal.tsx and livekit-bottom-sheet.tsx call it without awaiting. Although internal try-catch blocks handle errors, callers that don't await may experience unhandled rejections if errors occur. The interface should reflect the async implementation.

🔧 Suggested type fix
   // Room operations
   connectToRoom: (roomInfo: DepartmentVoiceChannelResultData, token: string) => Promise<void>;
-  disconnectFromRoom: () => void;
+  disconnectFromRoom: () => Promise<void>;
🤖 Fix all issues with AI agents
In `@electron/main.js`:
- Around line 183-201: The protocol handler accepts a user-controlled pathname
which can contain ".." and escape distPath; fix by resolving the final path with
path.resolve(distPath, decodeURIComponent(url.pathname)) and verify the resolved
path starts with the resolved distPath (e.g., const resolvedDist =
path.resolve(distPath)) before serving, falling back to index.html on mismatch;
also import pathToFileURL from 'url' and replace the net.fetch('file://' +
filePath) call with net.fetch(pathToFileURL(filePath).toString()) for correct
cross-platform file URLs in the protocol.handle callback.

In `@scripts/patch-nativewind.py`:
- Around line 42-74: The script currently does a full-file overwrite of
useColorScheme.js by writing patched_hook to use_color_scheme_path which can
silently clobber upstream changes; instead change Patch 2 to perform a targeted
replacement (e.g., locate and replace only the problematic block within
useColorScheme.js) or add a file-hash/version gate before writing: read the
existing useColorScheme.js, verify its hash or a known marker and only apply a
precise replace of the specific function/body (referencing useColorScheme,
prevEffect, cleanupEffect, and colorScheme.get/set/toggle) or abort with a clear
error if the file differs so you don't overwrite unexpected upstream updates.
- Around line 12-15: The current calls to content.replace(...) in
scripts/patch-nativewind.py can silently no-op if the target string changes;
after performing each substitution (e.g., the darkModeFlag change where you
replace the literal 'const darkModeFlag =
stylesheet_1.StyleSheet.getFlag("darkMode");'), assert the replacement actually
occurred by comparing the new string to the original and raise/exit with a clear
error if unchanged; apply this guard pattern to every replace() call in the
script (including the other calls you noted around the other patches) so each
substitution is verified and the script fails loudly when a patch didn't apply.
- Around line 4-7: The hardcoded absolute path should be replaced by a path
derived from the script location: compute a project_root (e.g., based on
os.path.dirname(__file__) and os.path.join(.., '..' as needed)) and then build
color_scheme_path using os.path.join(project_root, 'node_modules',
'react-native-css-interop', 'dist', 'runtime', 'web', 'color-scheme.js'); make
the same change for use_color_scheme_path and api_path so all three
(color_scheme_path, use_color_scheme_path, api_path) are constructed relative to
the script file or an optional CLI argument for root.

In `@src/app/call/`[id].tsx:
- Line 28: The import securityStore is used like a React hook but lacks the
conventional "use" prefix and triggers react-hooks rules; update the import to
alias securityStore as useSecurityStore (so it's consistent with other hooks
like useCallDetailStore/useCoreStore), then replace calls to securityStore(...)
with useSecurityStore(...) such as where const canUserCreateCalls is derived,
ensuring the hook naming convention is followed across the file.

In `@src/components/sidebar/sidebar-content.tsx`:
- Around line 66-88: The mapping over activeStatuses.Statuses can crash because
invertColor(status.BColor, true) throws for null/invalid hex; update the
rendering in sidebar-content.tsx to compute a safe text color before passing it
to ButtonText: either add a small validator (or reuse an existing isValidHex
helper in src/lib/utils.ts) and if status.BColor is missing/invalid fall back to
a default color (e.g., '#000000' or computed via a safeInvert fallback), or wrap
the invertColor call in try/catch and use the fallback on error; ensure you only
change the color computation (the Button/Text props and setIsOpen(status) usage
remain the same so the UI and action are preserved.
- Line 38: Replace the hardcoded unitName string with a translated value: change
the SidebarUnitCard prop unitName="No Unit" to use t(...) (e.g.,
unitName={t('common.no_unit')} or the appropriate key), ensuring the t function
from react-i18next is in scope; update the unitName prop usage in the
SidebarUnitCard component invocation so all user-facing text is wrapped with
t().

In `@src/components/status/status-bottom-sheet.tsx`:
- Line 8: Remove the unused import "useShallow" from 'zustand/react/shallow' in
the StatusBottomSheet component file; locate the import line that includes
useShallow (the one at the top of src/components/status/status-bottom-sheet.tsx)
and delete only that identifier so the file no longer imports an unused
symbol—no other code changes are needed.

In `@src/components/ui/skeleton/index.tsx`:
- Around line 24-64: customTimingFunction (created via Easing.bezier(0.4, 0,
0.6, 1)) is recreated every render which causes the useEffect that controls the
Animated.loop to restart continuously; fix by memoizing or hoisting that easing
function so its reference is stable (e.g., move Easing.bezier(...) outside the
component or wrap it in useMemo) and then remove the unstable
customTimingFunction from the effect dependencies (or keep the stable reference
there) so the animation lifecycle in the useEffect (which references pulseAnim,
animationDuration, isLoaded, isWeb, animRef) only runs when meaningful inputs
change.

In `@src/lib/countly-shim.web.ts`:
- Line 18: The initWithConfig function currently types its parameter as any;
change its signature to use the existing CountlyConfig type (preserving
optionality if needed) so the parameter becomes (_config?: CountlyConfig) and
update any internal references to rely on the typed shape; refer to the
initWithConfig method and the CountlyConfig type in this file when making the
change.

In `@src/lib/native-module-shims.web.ts`:
- Around line 188-258: The Countly shim duplicate should be removed and
re-exported from the existing module to avoid divergence: replace the local
Countly object with a direct re-export of the canonical export from the existing
countly-shim.web.ts so the exported symbol Countly (and associated types like
CountlyConfig and method initWithConfig) come from that single source of truth;
update any references in this file to export/import the same named export rather
than redefining functions like init, start, getCurrentDeviceId, etc., ensuring
TypeScript types align with CountlyConfig and initWithConfig from the original
module.

In `@src/services/notification-sound.service.ts`:
- Around line 49-54: performInitialization returns early on web leaving sound
properties null which causes playNotificationSound to emit repeated logger.warn
messages; update playNotificationSound (and/or its helper methods) to check
Platform.OS === 'web' and return silently when on web (or suppress the
"Notification sound not loaded" warning when Platform.OS === 'web'), referencing
the performInitialization and playNotificationSound functions and the
logger.warn call so the guard is added before any null-check warning is logged.

In `@src/stores/app/loading-store.ts`:
- Around line 47-48: The hook currently subscribes to the entire loadingStates
map via useLoadingStore((s) => s.loadingStates) causing all consumers to
re-render; change the selector to pick the specific primitive for the requested
key (e.g., useLoadingStore((s) => s.loadingStates[key] ?? false)) and assign
that directly to loading so only updates to that key trigger re-renders (update
the selector call in the useLoading / loading-store code that references
loadingStates and key).
- Line 52: toggleLoading currently closes over the stale loading value via
useCallback, causing rapid consecutive calls to cancel each other; fix by moving
the toggle logic into the store or by reading the fresh value from the store
before updating: add a store action (e.g., toggleLoading) that uses set(state =>
({ loadingStates: { ...state.loadingStates, [key]: !state.loadingStates[key]
}})) and replace the hook's useCallback toggleLoading (which calls
setLoading(key, !loading)) with a call to the new store.toggleLoading(key) (or,
if not adding a store action, have the hook call setLoading with a function that
reads the current store value via get() to compute the negation) so updates use
the latest state rather than the render-time captured loading.
🧹 Nitpick comments (36)
src/components/push-notification/push-notification-modal.tsx (2)

83-83: Avoid {...({} as any)} — this suppresses type errors with any.

This empty-object-cast-to-any spread is a type workaround that hides the actual type mismatch between the Modal component's props and what's being passed. If this is needed to work around a gluestack-ui typing issue, consider a more targeted suppression (e.g., @ts-expect-error with a comment explaining why) so future type fixes aren't silently swallowed.

Suggested alternative
-    <Modal isOpen={isOpen} onClose={handleClose} size="md" {...({} as any)}>
+    {/* `@ts-expect-error` gluestack-ui Modal type mismatch — remove when types are fixed */}
+    <Modal isOpen={isOpen} onClose={handleClose} size="md">

As per coding guidelines: "Avoid using any; strive for precise types".


14-29: Consider wrapping NotificationIcon in React.memo.

Since this component receives a simple type prop and renders pure output, memoizing it prevents unnecessary re-renders when the parent re-renders for unrelated reasons.

-const NotificationIcon = ({ type }: { type: NotificationType }) => {
+const NotificationIcon = React.memo(({ type }: { type: NotificationType }) => {
   // ...
-};
+});

As per coding guidelines: "Use React.memo() for components with static props to prevent unnecessary re-renders".

src/components/sidebar/unit-sidebar.tsx (2)

21-21: Consider wrapping in React.memo to avoid parent-triggered re-renders.

The component's props (unitName, unitType, unitGroup, bgColor) are likely stable strings from the parent. Wrapping in React.memo would skip re-renders caused by parent updates when these props haven't changed.


98-139: callButton, mapLockButton, and audioStreamButton styles are identical — consider consolidating.

Same for their *Active counterparts. A single shared style (e.g., iconButton / iconButtonActive) would reduce duplication.

src/components/ui/gluestack-ui-provider/index.web.tsx (2)

53-60: MediaQueryList.addListener/removeListener are deprecated.

These were deprecated in favor of addEventListener/removeEventListener. While this appears to be pre-existing code, consider updating since this is a web-specific file.

Suggested fix
-    media.addListener(handleMediaQuery);
+    media.addEventListener('change', handleMediaQuery);
 
-    return () => media.removeListener(handleMediaQuery);
+    return () => media.removeEventListener('change', handleMediaQuery);

62-75: Consider adding cssVariablesWithMode to the dependency array.

Line 70 references cssVariablesWithMode but it's not listed in the deps at Line 75. Since the value is memoized with [] it won't cause extra runs, but including it satisfies the exhaustive-deps lint rule and makes the dependency explicit.

src/components/ui/shared-tabs.tsx (2)

143-143: Anonymous arrow functions in onPress inside map loops.

Each render creates new function references per tab, which can cause unnecessary re-renders of Pressable children. Consider extracting a memoized TabItem sub-component that receives index and onPress as props.

As per coding guidelines: "Avoid anonymous functions in renderItem or event handlers to prevent re-renders."

Also applies to: 161-161


120-135: StyleSheet.create called inside a render-path function.

getContainerStyle calls StyleSheet.create on every render. StyleSheet.create is meant to be called once (typically at module scope) to register styles. Move the style computation to useMemo or use plain style objects if they need to be dynamic.

patches/@gluestack-ui+nativewind-utils+1.0.28.patch (2)

11-23: shallowEqual is duplicated four times across the patched files.

Since this is a patch-package patch where each file must be self-contained, extracting to a shared module would require patching an additional file (e.g., utils). Consider patching the library's existing utils module to export shallowEqual once, then importing it in each file — this would reduce drift if the logic ever needs updating.

Also applies to: 65-77, 126-138, 184-196


34-44: Ref writes during render are technically unsafe under React concurrent features.

Mutating mergedRef.current, prevParentRef.current, and prevContextRef.current during the render phase can lead to torn reads if React discards and retries a render (e.g., with useTransition or Suspense). The idiomatic alternative is useMemo:

const mergedValue = React.useMemo(
  () => ({ ...parentContextValues, [scope]: context }),
  // only recompute when values actually change:
  [scope, context, parentContextValues]
);

However, this still creates a new object when parentContextValues is a new reference (which is the original problem). Because the realistic impact here is limited to an unnecessary re-render rather than data corruption, and this is a third-party library patch for performance, the current approach is pragmatic. Just be aware of the trade-off if concurrent features are enabled.

Also applies to: 97-107

src/stores/app/loading-store.ts (1)

54-62: The useMemo wrapper adds complexity without meaningful benefit here.

The returned object is lightweight (a boolean + three stable callbacks). Wrapping it in useMemo saves one shallow object allocation per render but introduces its own overhead and dependency array to maintain. This is optional — feel free to keep it if you prefer the pattern for consistency across the codebase.

src/app/_layout.tsx (1)

57-57: Minor: remove stale commented-out code.

The //!isRunningInExpoGo() fragment is now dead. Consider removing it to keep the line clean.

Suggested cleanup
-  enableNativeFramesTracking: Platform.OS !== 'web', //!isRunningInExpoGo(), // Tracks slow and frozen frames in the application
+  enableNativeFramesTracking: Platform.OS !== 'web', // Tracks slow and frozen frames in the application
scripts/patch-nativewind.py (1)

9-11: No error handling if the target files don't exist.

If node_modules hasn't been installed yet or the package version doesn't include these files, the script will crash with an unhelpful FileNotFoundError. A pre-check with a clear message would improve DX.

Proposed guard
+for path in [color_scheme_path, use_color_scheme_path, api_path]:
+    if not os.path.isfile(path):
+        print(f"⚠️  Target not found, skipping patch: {path}")
+        sys.exit(1)
+
 with open(color_scheme_path, 'r') as f:
     content = f.read()
src/components/ui/drawer/index.tsx (1)

13-14: Dimensions.get('window') is captured once at module load time — won't react to resize/rotation.

screenWidth and screenHeight are computed at import time. On web (and on rotation on native), the drawer slide distances will use stale values. Consider using useWindowDimensions() inside DrawerContent instead if responsive behavior is needed.

src/lib/logging/index.tsx (1)

1-10: Consider reusing isWeb from src/lib/platform.ts.

src/lib/platform.ts already exports isWeb with the same Platform.OS === 'web' check. Reusing it would reduce duplication, unless there's a circular dependency concern with importing from platform into the logging module.

src/components/bluetooth/bluetooth-audio-modal.tsx (1)

287-287: Anonymous function in onPress inside a mapped list.

onPress={() => handleConnectDevice(device)} creates a new closure on every render for each device item, which can cause unnecessary re-renders. Consider extracting a memoized device-item component that receives device and onConnect as props. As per coding guidelines, "Avoid anonymous functions in renderItem or event handlers to prevent re-renders."

src/components/contacts/contact-notes-list.tsx (1)

239-241: Per-field selectors look good; consider a more targeted contactNotes selector.

The migration is correct. However, selecting the entire contactNotes object means this component re-renders whenever any contact's notes change. Since contactId is available as a prop, you could narrow the subscription:

-  const contactNotes = useContactsStore((s) => s.contactNotes);
+  const notes = useContactsStore((s) => s.contactNotes[contactId] || []);

This would also simplify downstream usage (removing line 249). That said, the current approach works fine and matches the broader refactor pattern.

nginx.conf (1)

41-43: CSP uses 'unsafe-eval' — verify this is required by the app bundle.

'unsafe-eval' in script-src significantly weakens XSS protection. This is sometimes needed by bundlers or libraries (e.g., some Expo Web builds), but if the app can run without it, removing it would harden the policy. The comment on Lines 41-42 mentions that docker-entrypoint.sh can tighten the policy — consider doing so for production.

Also, connect-src 'self' https: wss: permits connections to any origin, which effectively disables connect-src restrictions. If the set of backend/API origins is known, narrowing this would be preferable.

src/stores/security/store.ts (2)

23-29: JSON.stringify for equality check — acceptable here but has caveats.

This works for plain data objects, but be aware that property order differences or undefined values can cause false negatives. For a small rights object this is pragmatic. If the response shape ever includes dates or complex types, consider a dedicated shallow-compare utility instead.


46-58: isUserGroupAdmin creates a new closure on every call.

Since useSecurityStore returns a new isUserGroupAdmin function reference each time rights changes (or even on every render if the parent re-renders), any consumer that includes it in a dependency array will see unnecessary re-triggers. Consider memoizing it or documenting that consumers should call it inline only.

src/services/signalr.service.ts (1)

616-618: removeAllListeners is a clean addition for listener cleanup.

This complements the pre-connection cleanup pattern in the signalR store. Consider also cleaning up in disconnectFromHub to prevent stale listeners from accumulating if a hub is disconnected without explicit cleanup.

src/components/calls/call-files-modal.tsx (1)

64-74: Analytics useEffect depends on the full callFiles array reference.

Unlike other tracking effects in this PR that depend on .length, this one includes callFiles directly. Since fetchCallFiles creates a new array on each fetch, this effect re-fires every time files are loaded—even if the content hasn't changed. Consider using callFiles?.length if you only need the count.

Proposed fix
-  }, [isOpen, trackEvent, callId, callFiles, isLoadingFiles, errorFiles]);
+  }, [isOpen, trackEvent, callId, callFiles?.length, isLoadingFiles, errorFiles]);
docker/docker-entrypoint.sh (1)

25-25: ANDROID_VERSION_CODE is hardcoded to 1 without an env-var override.

Every other field follows the ${UNIT_X:-default} pattern for runtime configurability. If this is intentional for web-only deployments, consider adding a brief comment. Otherwise, make it configurable:

Proposed fix (if configurability is desired)
-  ANDROID_VERSION_CODE: 1,
+  ANDROID_VERSION_CODE: ${UNIT_ANDROID_VERSION_CODE:-1},

And add to the Dockerfile ENV block:

+    UNIT_ANDROID_VERSION_CODE=1 \
src/components/maps/map-pins.tsx (1)

16-29: Good memoization pattern for per-pin rendering.

One minor note: the key prop on Mapbox.MarkerView (Line 23) is redundant since MapPin itself already receives a key at the call site (Line 35), and there's only one MarkerView per MapPin. If id is sufficient for Mapbox's internal tracking, the key can be dropped from the MarkerView.

src/services/location.ts (1)

382-401: Unnecessary branching and as any cast in stopLocationUpdates.

Since the web shim at line 182 is already cast to Location.LocationSubscription, calling await this.locationSubscription.remove() works for both web (sync removeawait on a non-Promise resolves immediately) and native. The explicit web/native branching and as any cast can be removed. As per coding guidelines, any should be avoided.

♻️ Simplify by removing the platform branch
   async stopLocationUpdates(): Promise<void> {
     if (this.locationSubscription) {
-      if (isWeb) {
-        // On web the subscription is our own shim wrapping clearWatch
-        (this.locationSubscription as any).remove();
-      } else {
-        await this.locationSubscription.remove();
-      }
+      await this.locationSubscription.remove();
       this.locationSubscription = null;
     }
 
-    if (!isWeb) {
-      await this.stopBackgroundUpdates();
+    if (!isWeb) {
+      await this.stopBackgroundUpdates();
 
-      // Check if task is registered before stopping
-      const isTaskRegistered = await TaskManager.isTaskRegisteredAsync(LOCATION_TASK_NAME);
-      if (isTaskRegistered) {
-        await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME);
-      }
+      // Check if task is registered before stopping
+      const isTaskRegistered = await TaskManager.isTaskRegisteredAsync(LOCATION_TASK_NAME);
+      if (isTaskRegistered) {
+        await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME);
+      }
     }
src/stores/signalr/signalr-store.ts (2)

54-57: Good: Pre-connection listener cleanup prevents handler accumulation.

This correctly removes stale listeners before re-registering, preventing duplicate handlers across reconnections.

However, Biome flags the forEach callback because removeAllListeners returns a value. Use a block body to suppress the lint warning.

🧹 Suppress Biome lint warning
-      updateEvents.forEach((event) => signalRService.removeAllListeners(event));
+      updateEvents.forEach((event) => { signalRService.removeAllListeners(event); });

143-145: Same forEach return-value lint issue as with update events.

🧹 Suppress Biome lint warning
-      geoEvents.forEach((event) => signalRService.removeAllListeners(event));
+      geoEvents.forEach((event) => { signalRService.removeAllListeners(event); });
src/services/bluetooth-audio.service.ts (1)

66-73: Inconsistent web guard: uses Platform.OS === 'web' instead of this.isWeb.

All other web guards in this class use the this.isWeb field (Line 49). This one checks Platform.OS === 'web' directly, which is functionally equivalent but inconsistent.

♻️ Proposed fix
-    if (Platform.OS === 'web') {
+    if (this.isWeb) {
src/components/status/status-bottom-sheet.tsx (1)

70-76: Consider batching location store selectors with useShallow.

Seven individual selectors on useLocationStore means seven independent subscriptions. Since the location store sets all these properties in a single set() call via setLocation, each update will trigger up to seven re-renders of this component (one per property change notification). Since useShallow is already imported (though currently unused), you could batch these into a single selector:

♻️ Proposed alternative using useShallow
-  const latitude = useLocationStore((state) => state.latitude);
-  const longitude = useLocationStore((state) => state.longitude);
-  const heading = useLocationStore((state) => state.heading);
-  const accuracy = useLocationStore((state) => state.accuracy);
-  const speed = useLocationStore((state) => state.speed);
-  const altitude = useLocationStore((state) => state.altitude);
-  const timestamp = useLocationStore((state) => state.timestamp);
+  const { latitude, longitude, heading, accuracy, speed, altitude, timestamp } = useLocationStore(
+    useShallow((state) => ({
+      latitude: state.latitude,
+      longitude: state.longitude,
+      heading: state.heading,
+      accuracy: state.accuracy,
+      speed: state.speed,
+      altitude: state.altitude,
+      timestamp: state.timestamp,
+    }))
+  );
src/components/maps/map-view.web.tsx (1)

323-329: Comment says "layout effect" but this is a regular useEffect.

The comment on Line 324 says "Using a layout effect" but the code uses useEffect, not useLayoutEffect. For rendering into an external DOM node (mapbox-gl marker), useEffect is generally fine, but the comment should be corrected to avoid confusion.

📝 Proposed fix
-  // Render children into the marker's React root whenever children identity changes.
-  // Using a layout effect with [children] dep so it only fires when children actually change.
+  // Render children into the marker's React root whenever children identity changes.
   useEffect(() => {
src/stores/app/livekit-store.ts (1)

412-437: Mute sync condition is correct but hard to follow — consider inverting for clarity.

The condition isMicrophoneEnabled === muted is logically correct (it detects a mismatch between mic state and CallKeep muted state), but the inline comments spanning 8 lines suggest it's confusing. A simpler expression would improve readability:

♻️ Suggested simplification
-      if (room.localParticipant.isMicrophoneEnabled === muted) {
-             // If CallKeep says "muted" (true), and Mic is enabled (true), we need to disable mic.
-             // If CallKeep says "unmuted" (false), and Mic is disabled (false), we need to enable mic.
-             // Wait, logic check:
-             // isMicrophoneEnabled = true means NOT MUTED.
-             // muted = true means MUTED.
-             // So if isMicrophoneEnabled (true) and muted (true) -> mismatch, we must mute.
-             // if isMicrophoneEnabled (false) and muted (false) -> mismatch, we must unmute.
-             
-             // Actually effectively: setMicrophoneEnabled(!muted)
-             await room.localParticipant.setMicrophoneEnabled(!muted);
+      // Sync mic state: mic enabled means "not muted", so they're out of sync
+      // when isMicrophoneEnabled equals the muted flag.
+      const isOutOfSync = room.localParticipant.isMicrophoneEnabled === muted;
+      if (isOutOfSync) {
+             await room.localParticipant.setMicrophoneEnabled(!muted);
src/app/(app)/index.tsx (1)

388-389: Prefer ternary ? : over && for conditional rendering.

Per coding guidelines, use the conditional operator ? : instead of && for conditional rendering. This applies here and at line 399 (locationHeading !== null && ...).

As per coding guidelines: "Use ternary operator ? : for conditional rendering instead of &&".

♻️ Example for the outer conditional
-          {locationLatitude && locationLongitude && (
-            <Mapbox.PointAnnotation ...>
+          {locationLatitude && locationLongitude ? (
+            <Mapbox.PointAnnotation ...>
               ...
             </Mapbox.PointAnnotation>
-          )}
+          ) : null}
src/app/(app)/_layout.tsx (3)

300-305: Use lucide-react-native icons directly instead of the gluestack-ui Icon wrapper.

These icon renderers use <Icon as={Map} stroke={color} ...> which wraps lucide icons in the gluestack-ui Icon component. As per coding guidelines: "Use lucide-react-native for icons directly in markup; do not use the gluestack-ui icon component."

♻️ Example fix for one icon
- const mapIcon = useCallback(({ color }: { color: string }) => <Icon as={Map} stroke={color} className="text-primary-500 dark:text-primary-400" />, []);
+ const mapIcon = useCallback(({ color }: { color: string }) => <Map size={24} color={color} />, []);

243-260: Ref mutation during render — consider moving to an effect or memoization.

Updating lastNovuConfig.current directly during render (lines 253–260) is a side effect that React's concurrent mode documentation advises against. While it's practically safe here because the value is derived deterministically from state, it could cause subtle issues with concurrent features in React 19.

♻️ Alternative: derive via useMemo
- const lastNovuConfig = useRef<{...} | null>(null);
- if (novuReady) {
-   lastNovuConfig.current = { ... };
- }
+ const novuConfig = useMemo(() => {
+   if (!novuReady) return null;
+   return {
+     subscriberId: `${rights?.DepartmentCode}_Unit_${activeUnitId}`,
+     applicationIdentifier: config!.NovuApplicationId,
+     backendUrl: config!.NovuBackendApiUrl,
+     socketUrl: config!.NovuSocketUrl,
+   };
+ }, [novuReady, rights?.DepartmentCode, activeUnitId, config]);
+
+ // Cache last known-good config so provider stays mounted during transient nulls
+ const lastNovuConfig = useRef(novuConfig);
+ if (novuConfig) lastNovuConfig.current = novuConfig;

Note: The ref update still happens during render in this refactor, but the useMemo makes the derivation explicit and cacheable. If full purity is needed, move the ref update into a useEffect.


456-456: Prefer ternary ? : over && for conditional rendering.

As per coding guidelines: "Use ternary operator ? : for conditional rendering instead of &&".

♻️ Suggested fix
-          {isInitComplete && novuReady && <NotificationInbox isOpen={isNotificationsOpen} onClose={handleCloseNotifications} />}
+          {isInitComplete && novuReady ? <NotificationInbox isOpen={isNotificationsOpen} onClose={handleCloseNotifications} /> : null}
metro.config.js (1)

62-78: Document zustand version dependency for path-based module resolution.

The hard-coded CJS paths assume zustand 4.5.5's export structure (e.g., node_modules/zustand/index.js). While this version does publish CJS at these paths via conditional exports, future versions (e.g., zustand 5.x) could reorganize their exports and silently break this resolver. Add a comment noting the supported zustand version constraint or consider validating these paths exist at build time to catch version mismatches early.

import { useLocationStore } from '@/stores/app/location-store';
import { useCallDetailStore } from '@/stores/calls/detail-store';
import { useSecurityStore } from '@/stores/security/store';
import { securityStore } from '@/stores/security/store';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find the store definition
cat src/stores/security/store.ts | head -30

Repository: Resgrid/Unit

Length of output: 1064


🏁 Script executed:

# Check how other stores are defined and used
fd -t f "store.ts" src/stores --exec head -20 {} +

Repository: Resgrid/Unit

Length of output: 18582


🏁 Script executed:

# Look for eslint configurations
fd -t f "eslint" . -o fd -t f ".eslintrc*" .

Repository: Resgrid/Unit

Length of output: 179


🏁 Script executed:

# Check how securityStore is imported and used across the codebase
rg "securityStore" --type=ts --type=tsx -B2 -A2

Repository: Resgrid/Unit

Length of output: 83


🏁 Script executed:

# Check for other store imports with 'use' prefix
rg "import.*use.*Store.*from.*stores" --type=ts --type=tsx

Repository: Resgrid/Unit

Length of output: 83


🏁 Script executed:

cat src/stores/security/store.ts

Repository: Resgrid/Unit

Length of output: 1958


🏁 Script executed:

# Check the specific import in the file being reviewed
grep -n "import.*securityStore\|import.*useSecurityStore" src/app/call/\[id\].tsx

Repository: Resgrid/Unit

Length of output: 115


🏁 Script executed:

# Verify if useSecurityStore is exported and how it's used elsewhere
rg "useSecurityStore|securityStore" src/stores/security/store.ts -A2 -B2

Repository: Resgrid/Unit

Length of output: 338


🏁 Script executed:

# Check signalr-store to see how it imports both
cat src/stores/signalr/signalr-store.ts | grep -A5 "securityStore\|useSecurityStore"

Repository: Resgrid/Unit

Length of output: 550


🏁 Script executed:

# Check how securityStore is used in [id].tsx around line 61
sed -n '50,75p' src/app/call/\[id\].tsx

Repository: Resgrid/Unit

Length of output: 1566


securityStore should follow React hook naming conventions.

Line 61 calls securityStore as a React hook, but it lacks the use prefix that is consistent with other store hooks in this file (useCallDetailStore, useCoreStore, useStatusBottomSheetStore, useToastStore). This inconsistency will trigger react-hooks/rules-of-hooks ESLint warnings.

Import the store with a use prefix alias:

import { securityStore as useSecurityStore } from '@/stores/security/store';

Then use it as:

const canUserCreateCalls = useSecurityStore((state) => state.rights?.CanCreateCalls);

This aligns with the codebase convention of prefixing store hooks with use.

🤖 Prompt for AI Agents
In `@src/app/call/`[id].tsx at line 28, The import securityStore is used like a
React hook but lacks the conventional "use" prefix and triggers react-hooks
rules; update the import to alias securityStore as useSecurityStore (so it's
consistent with other hooks like useCallDetailStore/useCoreStore), then replace
calls to securityStore(...) with useSecurityStore(...) such as where const
canUserCreateCalls is derived, ensuring the hook naming convention is followed
across the file.

<VStack space="md" className="w-full flex-1 p-2">
{/* First row - Two cards side by side */}
<HStack space="md">
<SidebarUnitCard unitName="No Unit" unitType="" unitGroup={t('common.no_unit_selected')} bgColor="bg-background-50" />
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded "No Unit" string should be wrapped in t().

All user-facing text must use t() for internationalization. As per coding guidelines: "Ensure all text is wrapped in t() from react-i18next for translations."

Proposed fix
-          <SidebarUnitCard unitName="No Unit" unitType="" unitGroup={t('common.no_unit_selected')} bgColor="bg-background-50" />
+          <SidebarUnitCard unitName={t('common.no_unit')} unitType="" unitGroup={t('common.no_unit_selected')} bgColor="bg-background-50" />
📝 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
<SidebarUnitCard unitName="No Unit" unitType="" unitGroup={t('common.no_unit_selected')} bgColor="bg-background-50" />
<SidebarUnitCard unitName={t('common.no_unit')} unitType="" unitGroup={t('common.no_unit_selected')} bgColor="bg-background-50" />
🤖 Prompt for AI Agents
In `@src/components/sidebar/sidebar-content.tsx` at line 38, Replace the hardcoded
unitName string with a translated value: change the SidebarUnitCard prop
unitName="No Unit" to use t(...) (e.g., unitName={t('common.no_unit')} or the
appropriate key), ensuring the t function from react-i18next is in scope; update
the unitName prop usage in the SidebarUnitCard component invocation so all
user-facing text is wrapped with t().

const Countly = {
CountlyConfig,
init: async () => {},
initWithConfig: async (_config?: any) => {},
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Avoid any — use CountlyConfig type.

CountlyConfig is defined in the same file; use it instead of any.

-  initWithConfig: async (_config?: any) => {},
+  initWithConfig: async (_config?: CountlyConfig) => {},

As per coding guidelines, "Avoid using any; strive for precise types".

📝 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
initWithConfig: async (_config?: any) => {},
initWithConfig: async (_config?: CountlyConfig) => {},
🤖 Prompt for AI Agents
In `@src/lib/countly-shim.web.ts` at line 18, The initWithConfig function
currently types its parameter as any; change its signature to use the existing
CountlyConfig type (preserving optionality if needed) so the parameter becomes
(_config?: CountlyConfig) and update any internal references to rely on the
typed shape; refer to the initWithConfig method and the CountlyConfig type in
this file when making the change.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

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

⚠️ Outside diff range comments (19)
src/components/status/__tests__/status-gps-integration-working.test.tsx (1)

1-95: ⚠️ Potential issue | 🟡 Minor

Corrupted block comment — likely a merge or encoding artifact.

Lines 1–95 contain garbled, overlapping text fragments (e.g., expect.objec expect(mockSaveUni expect(mockSaveUnitStatus)...). While it's wrapped in a /* */ comment so it won't break at runtime, this is clearly corrupted content rather than intentional documentation. It should be replaced with a clean descriptive comment or removed entirely.

src/components/settings/__tests__/unit-selection-bottom-sheet.test.tsx (3)

210-217: ⚠️ Potential issue | 🟡 Minor

Toast store mock always invokes selector(state) without a type guard.

Unlike the other store mocks that check typeof selector === 'function', the toast store mock unconditionally calls selector(state). If the component (or any future code path) ever calls useToastStore() without a selector, this will throw a TypeError: selector is not a function.

🛡️ Proposed fix
     mockUseToastStore.mockImplementation((selector: any) => {
       const state = {
         showToast: mockShowToast,
         toasts: [],
         removeToast: jest.fn(),
       };
-      return selector(state);
+      return typeof selector === 'function' ? selector(state) : state;
     });

331-338: ⚠️ Potential issue | 🟡 Minor

Same inconsistency in beforeEach toast mock — apply the same guard.

This is the second instance of the toast store mock that unconditionally invokes selector(state).

🛡️ Proposed fix
     mockUseToastStore.mockImplementation((selector: any) => {
       const state = {
         showToast: mockShowToast,
         toasts: [],
         removeToast: jest.fn(),
       };
-      return selector(state);
+      return typeof selector === 'function' ? selector(state) : state;
     });

538-541: ⚠️ Potential issue | 🟡 Minor

Timing-dependent assertion may cause flaky tests.

await new Promise(resolve => setTimeout(resolve, 50)) followed by a synchronous assertion is inherently racy — CI under load could make this pass or fail nondeterministically. Consider wrapping the expectation in waitFor instead, or restructuring the test to avoid relying on wall-clock timing.

src/components/roles/__tests__/roles-bottom-sheet.test.tsx (2)

434-496: ⚠️ Potential issue | 🟠 Major

Tests duplicate component internals instead of exercising the actual component.

TestComponent re-implements the save/filtering logic from RolesBottomSheet. This means the test validates a copy of the logic — if the real component's filter changes (or regresses), these tests still pass. Instead, render <RolesBottomSheet>, interact with its Save button via fireEvent, and assert on mockAssignRoles calls. That way you're testing the actual code path.

The same concern applies to the whitespace-filtering test at Lines 550–584.


24-29: ⚠️ Potential issue | 🟡 Minor

Mock uses <div> instead of a React Native element.

In a React Native testing environment, <div> is not a valid host component. Use View from react-native to avoid potential warnings or failures depending on the test renderer configuration.

Proposed fix
 jest.mock('@/components/ui/bottom-sheet', () => ({
-  CustomBottomSheet: ({ children, isOpen }: any) => {
+  CustomBottomSheet: ({ children, isOpen }: { children: React.ReactNode; isOpen: boolean }) => {
+    const { View } = require('react-native');
     if (!isOpen) return null;
-    return <div>{children}</div>;
+    return <View>{children}</View>;
   },
 }));
src/components/roles/__tests__/roles-modal.test.tsx (1)

283-291: ⚠️ Potential issue | 🟡 Minor

Unused fireEvent import.

fireEvent is required on line 284 but never used in this test case. Remove it to avoid confusion.

Proposed fix
     it('should filter out roles with empty or whitespace RoleId and UserId', () => {
-      const { fireEvent } = require('@testing-library/react-native');
-
       render(<RolesModal isOpen={true} onClose={mockOnClose} />);
src/components/status/status-bottom-sheet.tsx (2)

538-538: ⚠️ Potential issue | 🟡 Minor

Redundant ternary — both branches produce the same value.

colorScheme === 'dark' ? '#fff' : '#fff' always evaluates to '#fff'. Same pattern on Line 648 and Line 658 with '#737373'. Simplify to a plain string.

Proposed fix
-                  <ArrowRight size={14} color={colorScheme === 'dark' ? '#fff' : '#fff'} />
+                  <ArrowRight size={14} color="#fff" />

Apply the same simplification at lines 648, 658, and 724.


499-501: ⚠️ Potential issue | 🟡 Minor

Hard-coded #ffffff fallback ignores dark mode.

When status.BColor is falsy and the item is not selected, the background is forced to white (#ffffff), which will look wrong in dark mode. Consider using a theme-aware fallback (e.g., colorScheme === 'dark' ? '#1f2937' : '#ffffff') or move the fallback to a className-based style so NativeWind's dark mode applies.

src/app/(app)/_layout.tsx (1)

149-159: ⚠️ Potential issue | 🟠 Major

isInitComplete is set to true even when initialization fails.

When initializeApp throws, hasInitialized is correctly reset to false, but isInitComplete is unconditionally set to true in the finally block. This removes the loading overlay and renders the drawer/sidebar/notification inbox against potentially uninitialized stores, which could cause runtime errors or a broken UI state.

Consider setting isInitComplete to true only on success, or introducing a separate error state to show a retry UI.

Proposed fix
     } catch (error) {
       logger.error({
         message: 'Failed to initialize app',
         context: { error },
       });
       // Reset initialization state on error so it can be retried
       hasInitialized.current = false;
     } finally {
       isInitializing.current = false;
-      setIsInitComplete(true);
     }
+
+    // Only reveal the full UI after successful init
+    setIsInitComplete(true);
src/components/status/__tests__/status-gps-integration.test.tsx (1)

59-62: ⚠️ Potential issue | 🟡 Minor

Debug console.log left in test setup.

Line 60 logs every useLocationStore call during test execution. This adds noise to test output and appears to be a leftover debugging artifact.

Proposed fix
     mockUseLocationStore.mockImplementation(() => {
-      console.log('useLocationStore called, returning:', mockLocationStore);
       return mockLocationStore;
     });
src/components/status/__tests__/status-gps-debug.test.tsx (1)

64-186: ⚠️ Potential issue | 🟡 Minor

This entire file appears to be a debugging/diagnostic test, not a production test.

The test contains 10+ console.log calls (lines 151–183), an unconditional screen.debug() (line 174), and its sole purpose is to verify a Submit button renders. All of this diagnostic output will pollute CI logs on every run. If the underlying issue has been resolved, consider removing this file entirely; otherwise, strip the debug statements and convert it into a proper focused test.

src/components/livekit/livekit-bottom-sheet.tsx (1)

58-73: ⚠️ Potential issue | 🟠 Major

Telemetry effect fires on every dependency change, not just on open.

The dependency array includes isConnected, isConnecting, currentView, isMuted, isTalking, etc. This means the livekit_bottom_sheet_opened event will be re-emitted every time any of these values change while the sheet is visible — not just when the sheet first opens. This can flood analytics with duplicate or misleading events.

Consider tracking only when isBottomSheetVisible transitions to true, using a ref to detect the transition:

Proposed fix
+ const wasVisibleRef = useRef(false);
+
  useEffect(() => {
-   if (isBottomSheetVisible) {
+   if (isBottomSheetVisible && !wasVisibleRef.current) {
      trackEvent('livekit_bottom_sheet_opened', {
        availableRoomsCount: availableRooms.length,
        isConnected: isConnected,
        isConnecting: isConnecting,
        currentView: currentView,
        hasCurrentRoom: !!currentRoomInfo,
        currentRoomName: currentRoomInfo?.Name || 'none',
        isMuted: isMuted,
        isTalking: isTalking,
        hasBluetoothMicrophone: selectedAudioDevices?.microphone?.type === 'bluetooth',
        hasBluetoothSpeaker: selectedAudioDevices?.speaker?.type === 'bluetooth',
      });
    }
+   wasVisibleRef.current = isBottomSheetVisible;
  }, [isBottomSheetVisible, trackEvent, availableRooms.length, isConnected, isConnecting, currentView, currentRoomInfo, isMuted, isTalking, selectedAudioDevices?.microphone?.type, selectedAudioDevices?.speaker?.type]);
src/stores/app/livekit-store.ts (1)

246-336: ⚠️ Potential issue | 🟡 Minor

Hardcoded English strings in Alert dialogs violate i18n guidelines.

Lines 306-308, 315, and 324 contain user-facing text that is not wrapped in t() from react-i18next. As per coding guidelines: "Wrap all user-facing text in t() from react-i18next".

Proposed fix (example for one Alert)
-                  Alert.alert(
-                    'Voice Permissions Required',
-                    'Phone state permission was permanently denied. Voice functionality may not work correctly. Please open Settings and grant the Phone permission for this app.',
-                    [
-                      { text: 'Cancel', style: 'cancel' },
-                      { text: 'Open Settings', onPress: () => Linking.openSettings() },
-                    ]
-                  );
+                  Alert.alert(
+                    t('permissions.voice_permissions_required'),
+                    t('permissions.phone_state_permanently_denied'),
+                    [
+                      { text: t('common.cancel'), style: 'cancel' },
+                      { text: t('permissions.open_settings'), onPress: () => Linking.openSettings() },
+                    ]
+                  );

Apply the same pattern to the other two Alert calls on Lines 315 and 324.

src/components/livekit/__tests__/livekit-bottom-sheet.test.tsx (1)

568-581: ⚠️ Potential issue | 🟡 Minor

This test validates the mock, not the component's behavior.

The test directly calls audioService.playConnectionSound() and asserts it was called — it doesn't verify that LiveKitBottomSheet invokes the audio service under the right conditions. This test provides false confidence; consider replacing it with a test that triggers a user action (e.g., mute toggle) and asserts the component calls the expected audio service method.

src/components/maps/map-view.web.tsx (1)

294-300: ⚠️ Potential issue | 🟠 Major

Anchor offset calculation reads zero-sized container.

At this point in the effect, createRoot has been called but root.render(children) hasn't executed yet (it runs in the separate effect on Line 332). The container has no content and getBoundingClientRect() will return { width: 0, height: 0 }, so the offset will always be [0, 0] regardless of the anchor prop.

To fix this, the offset needs to be computed after children are rendered into the container. One approach is to defer the offset calculation into the children-rendering effect (or use a ResizeObserver / requestAnimationFrame after the first render).

Suggested approach: compute offset after children render

Move the offset logic into the children-rendering effect (lines 332–336) or a dedicated post-render callback:

  // Render children into the marker's React root whenever children identity changes.
  useEffect(() => {
    if (containerRootRef.current && children) {
      containerRootRef.current.render(<>{children}</>);
+
+     // Recalculate anchor offset after children are rendered
+     if (typeof anchor === 'object' && anchor !== null && 'x' in anchor && 'y' in anchor && markerRef.current && containerRef.current) {
+       requestAnimationFrame(() => {
+         const rect = containerRef.current?.getBoundingClientRect();
+         if (rect && markerRef.current) {
+           const xOffset = (anchor.x - 0.5) * rect.width;
+           const yOffset = (anchor.y - 0.5) * rect.height;
+           markerRef.current.setOffset([xOffset, yOffset]);
+         }
+       });
+     }
    }
- }, [children]);
+ }, [children, anchor]);
src/features/livekit-call/store/__tests__/useLiveKitCallStore.test.ts (1)

507-554: ⚠️ Potential issue | 🟡 Minor

Duplicate nested describe('Error Handling') and duplicate test case.

There's a describe('Error Handling') at line 507 containing another describe('Error Handling') at line 508, and it('should handle basic error state management') appears twice (lines 530–541 and 543–553) with identical logic. This looks like a copy-paste mistake.

🐛 Proposed fix — remove the duplicate nesting
   describe('Error Handling', () => {
-  describe('Error Handling', () => {
     it('should handle room initialization errors', async () => {
       // ...existing test...
     });

     it('should handle basic error state management', async () => {
       // ...existing test...
     });
-  });
-
-    it('should handle basic error state management', async () => {
-      const { result } = renderHook(() => useLiveKitCallStore());
-
-      // Test basic error clearing functionality since token fetching isn't implemented
-      act(() => {
-        // Set an error state and then clear it
-        result.current.actions._clearError();
-      });
-
-      expect(result.current.error).toBeNull();
-    });
   });
src/app/(app)/__tests__/calls.test.tsx (1)

263-273: ⚠️ Potential issue | 🟡 Minor

Multiple tests contain only expect(true).toBe(true) — they validate nothing.

Tests at lines 263, 310, 322, 333, 347, 360, and 373 all rely on expect(true).toBe(true) as their primary assertion. These pass unconditionally and don't verify any component behavior. While the comments explain query limitations with HTML mocks, these are effectively dead tests.

Consider either:

  • Querying the rendered output to make real assertions (e.g., screen.queryByTestId, toJSON() snapshots)
  • Marking them as .todo tests until proper assertions can be added
src/app/(app)/index.tsx (1)

392-411: ⚠️ Potential issue | 🟠 Major

&& with numeric locationLatitude/locationLongitude can crash React Native

In React Native, {0 && <View />} attempts to render 0 as a bare text node, which throws "Text strings must be rendered within a <Text> component". Since locationLatitude and locationLongitude are numbers, if either is exactly 0 the short-circuit evaluates to 0 and RN crashes.

This affects:

  • Line 392: {locationLatitude && locationLongitude && (<Mapbox.PointAnnotation …>…</Mapbox.PointAnnotation>)}
  • Line 314: showRecenterButton resolves to locationLongitude (a number) when all preceding terms are truthy, then used in {showRecenterButton && (…)} at line 416.

Per coding guidelines, prefer the ternary operator (? :) for conditional rendering instead of &&.

Proposed fix — use explicit boolean coercion or null checks
-  const showRecenterButton = !isMapLocked && hasUserMovedMap && locationLatitude && locationLongitude;
+  const showRecenterButton = !isMapLocked && hasUserMovedMap && locationLatitude != null && locationLongitude != null;
-          {locationLatitude && locationLongitude && (
-            <Mapbox.PointAnnotation id="userLocation" coordinate={[locationLongitude, locationLatitude]} anchor={{ x: 0.5, y: 0.5 }}>
+          {locationLatitude != null && locationLongitude != null ? (
+            <Mapbox.PointAnnotation id="userLocation" coordinate={[locationLongitude, locationLatitude]} anchor={{ x: 0.5, y: 0.5 }}>
               ...
-            </Mapbox.PointAnnotation>
-          )}
+            </Mapbox.PointAnnotation>
+          ) : null}

Apply the same != null pattern to other guard expressions on lines 81, 137, 169, and 259.

As per coding guidelines: "Use ternary operator ? : for conditional rendering instead of &&".

Also applies to: 314-314

🤖 Fix all issues with AI agents
In `@electron/main.js`:
- Around line 185-212: The handler using path.resolve(distPath,
decodeURIComponent(url.pathname)) is wrong because an absolute pathname resets
the base so every request escapes distPath; change to first join the base and
request (use path.join(distPath, decodeURIComponent(url.pathname))) then
canonicalize (path.resolve or path.normalize) into resolvedPath, keep the same
security check against resolvedDist (and path.sep) and the fallback to
index.html; update references in protocol.handle('app') where resolvedPath,
filePath, distPath, resolvedDist, and url.pathname are used.

In `@src/app/`(app)/index.tsx:
- Around line 219-236: The Animated.loop started inside the useEffect (using
pulseAnim) never gets stopped, so capture the loop animation in a variable
(e.g., loopAnim = Animated.loop(...)) before calling .start(), and return a
cleanup function from the useEffect that calls loopAnim.stop() to halt the
animation when the component (MapContent) unmounts; keep the Platform.OS !==
'web' guard and the same Animated.sequence/timing configuration but ensure you
stop the same loopAnim instance in the cleanup.

In `@src/components/maps/map-view.web.tsx`:
- Around line 318-320: The dependency array for the effect that recreates the
marker is missing the "anchor" dependency referenced in the comment; update the
effect in map-view.web.tsx (the closure that uses map, id, and
markerOptions.anchor) to include anchor in the dependency array so the marker is
recreated when anchor changes, or if you intend not to trigger on anchor changes
then update the comment to remove "anchor" — modify the effect surrounding map
and id (the one that creates/updates the marker) to either add anchor to [map,
id] or correct the comment to match the actual dependencies.
- Around line 330-336: Update the misleading comment and ensure the marker is
cleared when children becomes falsy: change the comment that currently claims
"Using a layout effect" to accurately say "Using useEffect" (or similar) and
modify the effect that uses useEffect so that it always calls
containerRootRef.current.render(...) — rendering children when present and
rendering an empty fragment (or null-equivalent) when children is falsy — so
stale content is cleared; target the useEffect block around
containerRootRef.current.render(...) and the children variable in
map-view.web.tsx.
- Around line 9-10: Remove the unnecessary TypeScript suppression above the
import of createRoot: delete the "// `@ts-ignore` - react-dom/client types may not
be available" comment so the import line "import { createRoot } from
'react-dom/client';" uses the built-in React 19 type definitions without
silencing the compiler.

In `@src/components/push-notification/__tests__/push-notification-modal.test.tsx`:
- Line 111: The test mocks duplicate store objects in each mockImplementation
causing potential drift; instead create a single shared mock store object and
reuse it in the mock for usePushNotificationModalStore (and the other spots) so
both branches return the exact same object; implement a small helper (e.g.,
sharedMockStore or getMockedStore) and update the mockImplementation calls to
return that shared object when selector is a function or as the fallback,
referencing usePushNotificationModalStore, mockImplementation, and mockStore to
locate where to change.

In `@src/components/settings/bluetooth-device-selection-bottom-sheet.tsx`:
- Around line 280-281: The two hardcoded user-facing strings inside the Text
components (the label that checks preferredDevice?.id === 'system-audio' and the
subtitle "AirPods, Car, Wired Headset") must be wrapped with the i18n translator
t(); update the Text nodes in bluetooth-device-selection-bottom-sheet.tsx so the
primary label uses t('System Audio') (or an appropriate key) where
preferredDevice?.id === 'system-audio' is checked, and the secondary description
uses t('AirPods, Car, Wired Headset') (or a chosen translation key), ensuring
you import and use the t function from react-i18next in that component.

In `@src/components/sidebar/__tests__/roles-sidebar.test.tsx`:
- Line 243: The mock for useCoreStore is passing newUnit directly so
selector(state => state.activeUnit) receives a UnitResultData and returns
undefined; update the mockUseCoreStore implementation used in this test to pass
an object with activeUnit: newUnit (i.e.,
mockUseCoreStore.mockImplementation((selector: any) => typeof selector ===
'function' ? selector({ activeUnit: newUnit }) : { activeUnit: newUnit })),
ensuring the selector in the component (useCoreStore((state) =>
state.activeUnit)) sees the intended activeUnit value and the test exercises the
intended branch.

In `@src/components/sidebar/__tests__/unit-sidebar.test.tsx`:
- Line 59: The mock for useCoreStore is calling selector(...) unconditionally
which will throw if the hook is used without a selector; update
mockUseCoreStore.mockImplementation to mirror the other store mocks by checking
typeof selector === 'function' and returning selector({ activeUnit: null } as
any) only when it's a function, otherwise return the state object ({ activeUnit:
null } as any); reference mockUseCoreStore and the selector guard pattern used
elsewhere in this test file.

In `@src/services/location.ts`:
- Around line 156-185: The web branch silently no-ops when 'geolocation' is
missing and lacks a success log; update the isWeb path in start/subscribe logic
to (1) emit a logger.warn (or logger.error) when 'geolocation' is not present so
callers can diagnose unsupported environments, and (2) after successfully
creating the watch with navigator.geolocation.watchPosition and assigning
this.locationSubscription, call logger.info with a message like "Foreground
location updates started" (matching the native path) so successful subscription
is observable; use the existing this.locationSubscription, watchId, and logger
symbols to implement these logs.
- Line 13: The file defines a duplicate const isWeb (const isWeb = Platform.OS
=== 'web') even though isWeb is already exported; remove the local declaration
and instead import the shared exported symbol (isWeb) from the module that
provides it, replace usages to reference the imported isWeb, and remove any
now-unused Platform import to avoid duplication and dead code.

Comment on lines +9 to +10
// @ts-ignore - react-dom/client types may not be available
import { createRoot } from 'react-dom/client';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check package.json for `@types/react-dom` version and React version
fd "package.json" -t f --max-depth 2 -x cat {} | jq '{react: .dependencies.react // .devDependencies.react, "react-dom": .dependencies["react-dom"] // .devDependencies["react-dom"], "@types/react-dom": .dependencies["@types/react-dom"] // .devDependencies["@types/react-dom"], "@types/react": .dependencies["@types/react"] // .devDependencies["@types/react"]}'

Repository: Resgrid/Unit

Length of output: 2308


🏁 Script executed:

#!/bin/bash
# Read the actual file around lines 9-10
head -20 src/components/maps/map-view.web.tsx

Repository: Resgrid/Unit

Length of output: 743


🏁 Script executed:

#!/bin/bash
# Check if tsconfig.json exists and examine type checking settings
fd "tsconfig.json" -t f --max-depth 2 -x cat {}

Repository: Resgrid/Unit

Length of output: 1146


🏁 Script executed:

#!/bin/bash
# Read package.json and extract dependency versions
cat package.json | jq '.dependencies, .devDependencies' | grep -E '(react|@types/react)'

Repository: Resgrid/Unit

Length of output: 1826


🏁 Script executed:

#!/bin/bash
# Also try extracting specific versions more directly
cat package.json | jq -r '.dependencies["react-dom"], .devDependencies["react-dom"], .dependencies["@types/react-dom"], .devDependencies["@types/react-dom"]'

Repository: Resgrid/Unit

Length of output: 77


Remove unnecessary @ts-ignore for react-dom/client.

React 19.0.0 includes bundled TypeScript definitions for react-dom/client and does not require the @types/react-dom package. The @ts-ignore suppression can be safely removed.

🤖 Prompt for AI Agents
In `@src/components/maps/map-view.web.tsx` around lines 9 - 10, Remove the
unnecessary TypeScript suppression above the import of createRoot: delete the
"// `@ts-ignore` - react-dom/client types may not be available" comment so the
import line "import { createRoot } from 'react-dom/client';" uses the built-in
React 19 type definitions without silencing the compiler.

Comment on lines +318 to +320
// Only recreate marker when map, id, or anchor change — NOT children
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [map, coordinate, id, children, anchor, onSelected, title]);
}, [map, id]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Comment says anchor is a dependency but the array omits it.

The comment on Line 318 states "Only recreate marker when map, id, or anchor change" but the dependency array is [map, id]. If anchor type changes from object to string (or vice versa), the marker won't be recreated and the markerOptions.anchor won't be updated. Either add anchor to the dependency array or correct the comment.

Proposed fix
-    // Only recreate marker when map, id, or anchor change — NOT children
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [map, id]);
+    // Only recreate marker when map, id, or anchor change — NOT children
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [map, id, anchor]);
📝 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
// Only recreate marker when map, id, or anchor change — NOT children
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [map, coordinate, id, children, anchor, onSelected, title]);
}, [map, id]);
// Only recreate marker when map, id, or anchor change — NOT children
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [map, id, anchor]);
🤖 Prompt for AI Agents
In `@src/components/maps/map-view.web.tsx` around lines 318 - 320, The dependency
array for the effect that recreates the marker is missing the "anchor"
dependency referenced in the comment; update the effect in map-view.web.tsx (the
closure that uses map, id, and markerOptions.anchor) to include anchor in the
dependency array so the marker is recreated when anchor changes, or if you
intend not to trigger on anchor changes then update the comment to remove
"anchor" — modify the effect surrounding map and id (the one that
creates/updates the marker) to either add anchor to [map, id] or correct the
comment to match the actual dependencies.

Comment on lines +330 to +336
// Render children into the marker's React root whenever children identity changes.
// Using a layout effect with [children] dep so it only fires when children actually change.
useEffect(() => {
if (containerRootRef.current && children) {
containerRootRef.current.render(<>{children}</>);
}
}, [children]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Two minor concerns with the children-rendering effect.

  1. Misleading comment: Line 331 says "Using a layout effect" but the code uses useEffect, not useLayoutEffect. Update the comment to match.

  2. Stale children when children becomes falsy: The guard if (... && children) means that if children transitions from a valid element to null/undefined, the last rendered content will remain visible in the marker. Consider rendering an empty fragment when children is falsy to clear the marker content.

Proposed fix
- // Render children into the marker's React root whenever children identity changes.
- // Using a layout effect with [children] dep so it only fires when children actually change.
+ // Render children into the marker's React root whenever children change.
  useEffect(() => {
-   if (containerRootRef.current && children) {
-     containerRootRef.current.render(<>{children}</>);
+   if (containerRootRef.current) {
+     containerRootRef.current.render(<>{children}</>);
    }
  }, [children]);
🤖 Prompt for AI Agents
In `@src/components/maps/map-view.web.tsx` around lines 330 - 336, Update the
misleading comment and ensure the marker is cleared when children becomes falsy:
change the comment that currently claims "Using a layout effect" to accurately
say "Using useEffect" (or similar) and modify the effect that uses useEffect so
that it always calls containerRootRef.current.render(...) — rendering children
when present and rendering an empty fragment (or null-equivalent) when children
is falsy — so stale content is cleared; target the useEffect block around
containerRootRef.current.render(...) and the children variable in
map-view.web.tsx.

};

mockUseCoreStore.mockReturnValue(newUnit);
mockUseCoreStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector(newUnit) : newUnit);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Bug: mock state shape inconsistent with selector — test passes for the wrong reason.

The component uses useCoreStore((state) => state.activeUnit), so the selector expects an object with an activeUnit property. Here newUnit is passed directly (a UnitResultData), so selector(newUnit) evaluates to newUnit.activeUnitundefined. The test expects 0/0 Active which happens to match the "no active unit" path, not the "different unit with no assignments" path it intends to verify.

Every other mockUseCoreStore call in this file wraps the value correctly (e.g., line 89, 100).

🐛 Proposed fix
-    mockUseCoreStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector(newUnit) : newUnit);
+    mockUseCoreStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector({ activeUnit: newUnit }) : { activeUnit: newUnit });
📝 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
mockUseCoreStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector(newUnit) : newUnit);
mockUseCoreStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector({ activeUnit: newUnit }) : { activeUnit: newUnit });
🤖 Prompt for AI Agents
In `@src/components/sidebar/__tests__/roles-sidebar.test.tsx` at line 243, The
mock for useCoreStore is passing newUnit directly so selector(state =>
state.activeUnit) receives a UnitResultData and returns undefined; update the
mockUseCoreStore implementation used in this test to pass an object with
activeUnit: newUnit (i.e., mockUseCoreStore.mockImplementation((selector: any)
=> typeof selector === 'function' ? selector({ activeUnit: newUnit }) : {
activeUnit: newUnit })), ensuring the selector in the component
(useCoreStore((state) => state.activeUnit)) sees the intended activeUnit value
and the test exercises the intended branch.

@@ -57,14 +57,23 @@ describe('SidebarUnitCard', () => {

// Reset to default mocks
mockUseCoreStore.mockImplementation((selector) => selector({ activeUnit: null } as any));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent mock pattern: useCoreStore lacks the non-selector fallback.

Line 59 always calls selector(...) without checking if selector is a function. The other three stores in this same beforeEach (lines 60–80) use the typeof selector === 'function' guard. If useCoreStore is ever called without a selector in the component under test, this will throw a TypeError.

♻️ Suggested fix for consistency
-    mockUseCoreStore.mockImplementation((selector) => selector({ activeUnit: null } as any));
+    mockUseCoreStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector({ activeUnit: null } as any) : { activeUnit: null });
📝 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
mockUseCoreStore.mockImplementation((selector) => selector({ activeUnit: null } as any));
mockUseCoreStore.mockImplementation((selector: any) => typeof selector === 'function' ? selector({ activeUnit: null } as any) : { activeUnit: null });
🤖 Prompt for AI Agents
In `@src/components/sidebar/__tests__/unit-sidebar.test.tsx` at line 59, The mock
for useCoreStore is calling selector(...) unconditionally which will throw if
the hook is used without a selector; update mockUseCoreStore.mockImplementation
to mirror the other store mocks by checking typeof selector === 'function' and
returning selector({ activeUnit: null } as any) only when it's a function,
otherwise return the state object ({ activeUnit: null } as any); reference
mockUseCoreStore and the selector guard pattern used elsewhere in this test
file.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/app/`(app)/index.tsx:
- Around line 397-398: The conditional guards using logical && (e.g., the
Mapbox.PointAnnotation render guarded by "locationLatitude && locationLongitude
&& (...)") incorrectly treat 0 as falsy and can render "0"; replace these with
nullish checks and the conditional operator so each guard becomes
"(locationLatitude ?? locationLongitude) ? <...> : null" (specifically update
the Mapbox.PointAnnotation that references locationLatitude/locationLongitude
and apply the same pattern to the other guards at the same sites mentioned),
ensuring you check each coordinate with the nullish coalescing or explicit
null/undefined checks and use ? : to return the JSX or null.

In `@src/services/location.ts`:
- Around line 155-191: In startLocationUpdates(), update the web geolocation
error callback (the watcher created via navigator.geolocation.watchPosition) to
treat fatal permission errors by removing/clearing the stale subscription and
clearing this.locationSubscription so future startLocationUpdates() calls can
re-subscribe; specifically ensure the subscription wrapper’s remove() (which
calls navigator.geolocation.clearWatch) is invoked (or call clearWatch with the
captured watchId) and then set this.locationSubscription = undefined/null inside
the error handler, while keeping the existing logger call.
🧹 Nitpick comments (3)
electron/main.js (1)

200-209: Minor: catch swallows all statSync errors, not just ENOENT.

Permission errors or other filesystem issues would silently fall back to index.html rather than surfacing the real problem. This is unlikely in practice for a local packaged app, so it's a nit.

💡 Optional: narrow the catch
       } catch (err) {
-        // File not found - serve index.html for client-side routing
-        filePath = path.join(resolvedDist, 'index.html');
+        if (err.code === 'ENOENT') {
+          // File not found - serve index.html for client-side routing
+          filePath = path.join(resolvedDist, 'index.html');
+        } else {
+          // Unexpected error - log and fall back
+          console.error('Protocol handler error:', err);
+          filePath = path.join(resolvedDist, 'index.html');
+        }
       }
src/services/location.ts (1)

388-407: Unnecessary as any cast and redundant platform branch in stopLocationUpdates.

The web shim (line 187) is already cast to Location.LocationSubscription, so this.locationSubscription.remove() compiles without as any. Moreover, await-ing the synchronous web shim's remove() is harmless (resolves immediately), so the entire isWeb / else split is unnecessary.

This also violates the coding guideline: "Avoid using any; strive for precise types."

Suggested simplification
   async stopLocationUpdates(): Promise<void> {
     if (this.locationSubscription) {
-      if (isWeb) {
-        // On web the subscription is our own shim wrapping clearWatch
-        (this.locationSubscription as any).remove();
-      } else {
-        await this.locationSubscription.remove();
-      }
+      await this.locationSubscription.remove();
       this.locationSubscription = null;
     }

As per coding guidelines: "Avoid using any; strive for precise types."

src/app/(app)/index.tsx (1)

403-412: Same falsy-zero issue with locationHeading — and uses && conditional rendering.

locationHeading !== null && locationHeading !== undefined is a proper nullish check here ✓ — but this JSX conditional still uses && instead of the ternary operator per coding guidelines. This is a minor nit compared to the latitude/longitude issue above.

Suggested ternary pattern
-                  {locationHeading !== null && locationHeading !== undefined && (
-                    <View
-                      style={[
-                        styles.directionIndicator,
-                        {
-                          transform: [{ rotate: `${locationHeading}deg` }],
-                        },
-                      ]}
-                    />
-                  )}
+                  {locationHeading != null ? (
+                    <View
+                      style={[
+                        styles.directionIndicator,
+                        {
+                          transform: [{ rotate: `${locationHeading}deg` }],
+                        },
+                      ]}
+                    />
+                  ) : null}

As per coding guidelines: "Use the conditional operator (?:) for conditional rendering instead of &&".

Comment on lines 155 to +191
async startLocationUpdates(): Promise<void> {
// On web, use a lightweight browser geolocation watcher instead of expo-location/TaskManager
if (isWeb) {
if (!('geolocation' in navigator)) {
logger.warn({ message: 'Geolocation API not available in this browser' });
return;
}

if (!this.locationSubscription) {
const watchId = navigator.geolocation.watchPosition(
(pos) => {
const loc: Location.LocationObject = {
coords: {
latitude: pos.coords.latitude,
longitude: pos.coords.longitude,
altitude: pos.coords.altitude ?? 0,
accuracy: pos.coords.accuracy ?? 0,
altitudeAccuracy: pos.coords.altitudeAccuracy ?? 0,
heading: pos.coords.heading ?? 0,
speed: pos.coords.speed ?? 0,
},
timestamp: pos.timestamp,
};
useLocationStore.getState().setLocation(loc);
sendLocationToAPI(loc);
},
(err) => {
logger.warn({ message: 'Web geolocation error', context: { code: err.code, msg: err.message } });
},
{ enableHighAccuracy: false, maximumAge: 15000, timeout: 30000 }
);
// Store a compatible subscription object
this.locationSubscription = { remove: () => navigator.geolocation.clearWatch(watchId) } as unknown as Location.LocationSubscription;
logger.info({ message: 'Foreground location updates started' });
}
return;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Web geolocation path looks solid — previous observability concerns addressed.

The web branch now logs a warning when navigator.geolocation is unavailable (line 159) and logs success on subscription (line 188), resolving both prior review concerns.

One minor edge case: if the browser revokes geolocation permission after the watcher is established, the error callback (lines 181-183) logs the failure but leaves this.locationSubscription set. Subsequent calls to startLocationUpdates would see the stale subscription and skip re-subscribing (line 163 guard), silently stopping updates.

Suggested handling for fatal geolocation errors
           (err) => {
-            logger.warn({ message: 'Web geolocation error', context: { code: err.code, msg: err.message } });
+            logger.warn({ message: 'Web geolocation error', context: { code: err.code, msg: err.message } });
+            // PERMISSION_DENIED — clear the subscription so a future call can retry
+            if (err.code === err.PERMISSION_DENIED) {
+              if (this.locationSubscription) {
+                (this.locationSubscription as { remove: () => void }).remove();
+                this.locationSubscription = null;
+              }
+            }
           },
🤖 Prompt for AI Agents
In `@src/services/location.ts` around lines 155 - 191, In startLocationUpdates(),
update the web geolocation error callback (the watcher created via
navigator.geolocation.watchPosition) to treat fatal permission errors by
removing/clearing the stale subscription and clearing this.locationSubscription
so future startLocationUpdates() calls can re-subscribe; specifically ensure the
subscription wrapper’s remove() (which calls navigator.geolocation.clearWatch)
is invoked (or call clearWatch with the captured watchId) and then set
this.locationSubscription = undefined/null inside the error handler, while
keeping the existing logger call.

@ucswift
Copy link
Member Author

ucswift commented Feb 10, 2026

Approve

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

This PR is approved.

@ucswift ucswift merged commit e36beca into master Feb 10, 2026
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant