Skip to content

RC-T39 Dispatch fixes - #125

Merged
ucswift merged 2 commits into
masterfrom
develop
Aug 11, 2026
Merged

RC-T39 Dispatch fixes#125
ucswift merged 2 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Aug 11, 2026

Copy link
Copy Markdown
Member

PR Description: RC-T39 Dispatch Fixes

This PR delivers authorization controls, incident command chat integration, authentication reliability fixes, and significant performance improvements across the Dispatch app.

Authorization & Access Control

  • Dispatch app access restriction: Added CanLoginToDispatchApp permission. Users not authorized for Dispatch are now signed out during initialization with a clear toast message rather than landing in an empty app.
  • Incident Command access restriction: Added CanLoginToCommandApp permission. The Incident Command tab now shows a "not authorized" state instead of attempting to load boards that will fail with 403s.
  • Both permissions default to true for departments that haven't configured them.

Incident Command Chat

  • Added a Chat section to the Command Board view, giving dispatchers access to the incident channel, the dispatch channel, a direct message line to the current Incident Commander, and direct messages to all personnel holding ICS roles.
  • New useDirectMessage hook handles opening or reusing 1:1 conversations (server deduplicates).
  • Chat channels are loaded per-incident and kept separate from the main channel list so dispatchers' channel lists aren't buried under incident traffic.

Authentication Reliability

  • Single-flight token refresh: Created a shared token-refresh module so the automatic refresh timer and the axios 401 interceptor never rotate the refresh token in parallel (which would invalidate one request).
  • Fixed auto-refresh scheduling bug: The timer was firing immediately due to an incorrect expiry calculation; it now correctly schedules refresh one minute before expiration.
  • SAML CSRF protection: Added a RelayState nonce to the SAML flow, with pending-state persistence and replay protection, to reject unsolicited deep-link injections.
  • Token refresh timer is now cancelled on logout.

Session Lifecycle

  • Full session teardown on sign-out: Added teardownSignedInSession that disconnects SignalR hubs, LiveKit rooms, audio streams, check-in polling, chat timers, and push notification listeners — preventing resources from running against a signed-out session.

Performance Optimizations

  • Map screen: Extracted the user location marker into a memoized component and moved GPS updates out of the React render cycle via imperative store subscriptions, eliminating whole-screen re-renders on every GPS tick.
  • Calls screen: Switched to field-level store selectors and added an O(1) priority lookup map to prevent re-renders and redundant lookups per row.
  • Check-in, POI detail, call sidebar, SignalR lifecycle hook: Replaced whole-store and object-returning selectors with individual field selectors or imperative reads to reduce unnecessary re-renders.
  • Home screen (phone layout): Bounded panel heights so nested FlatLists can virtualize instead of rendering all rows at once.

Bug Fixes

  • SignalR reconnection: When max reconnect attempts are exhausted, the hub now schedules another reconnection instead of staying dead until a lifecycle event.
  • LiveKit room management: Clean disconnect of previous rooms (listener removal, awaited disconnect) before reconnecting; proper partial-room disposal on connect failure; always reset state on disconnect even if desynced.
  • Web audio stream listeners: Event listeners are now detached via AbortController on stop, preventing listener accumulation across play/stop cycles.

Summary by CodeRabbit

  • New Features

    • Added incident-specific chat, archived-channel access, and direct messaging from incident command views.
    • Added dispatch and command-app access permissions.
    • Added automatic session token renewal and stronger SAML sign-in protection.
    • Added localized content for chat, dispatch, incident command, check-ins, maps, onboarding, and video features.
  • Bug Fixes

    • Improved sign-out cleanup for calls, chat, audio, notifications, and live connections.
    • Improved map lock behavior, nested home-screen layouts, and connection retry reliability.
    • Prevented stale audio and video connections during reconnection.

@Resgrid-Bot

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change centralizes token refresh, adds SAML state validation, introduces incident chat channels and direct messaging, strengthens session cleanup, updates Zustand subscriptions, adjusts map and call rendering, aligns tests, and expands localized content.

Changes

Application runtime

Layer / File(s) Summary
Authentication and authorization lifecycle
src/lib/auth/*, src/stores/auth/*, src/hooks/use-saml-login.ts, src/app/(app)/_layout.tsx
Adds persisted SAML state validation, scheduled single-flight token refresh, refresh failure handling, and Dispatch/Command authorization checks.
Incident chat loading and access
src/api/chat/chat.ts, src/stores/chat/store.ts, src/components/incident-command/*, src/hooks/use-direct-message.ts, src/app/(app)/chat.tsx
Loads archived incident channels by call ID, renders incident and dispatch channels, supports direct messages, and uses object-based chat navigation.
Session resource teardown and reconnection
src/app/(app)/_layout.tsx, src/hooks/use-signalr-lifecycle.ts, src/services/signalr.service.ts, src/stores/app/audio-stream-store.web.ts, src/stores/app/livekit-store.ts
Adds teardown for signed-in resources, explicit SignalR retry scheduling, audio listener cleanup, and LiveKit room cleanup.
Store subscriptions and screen rendering
src/app/(app)/*, src/components/checkIn/*, src/components/pois/*, src/components/sidebar/*
Uses field-level store selectors, imperative location reads, memoized call rendering, bounded nested panels, and a separate memoized map location marker.
Store contracts and test fixtures
src/__tests__/*, src/hooks/__tests__/*, src/lib/__tests__/*, src/stores/security/__tests__/*
Updates selector-aware Zustand mocks, native-module and router mocks, token-refresh tests, and security rights fixtures.
Localized application content
src/translations/*
Adds or reorganizes chat, dispatch, incident command, check-in, authentication, map, video-feed, and related interface translations.

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

Possibly related PRs

Suggested reviewers: github-actions

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the PR as a set of Dispatch fixes and matches the main changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

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

⚠️ Outside diff range comments (2)
src/stores/app/audio-stream-store.web.ts (1)

211-216: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Detach listeners before checking audioElement.

When audio.play() rejects, audioElement remains null, so stopStream() skips detachStreamListeners() and leaves the failed stream's handlers attached. Call detachStreamListeners() unconditionally at the start of stopStream().

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

In `@src/stores/app/audio-stream-store.web.ts` around lines 211 - 216, Update
stopStream to call detachStreamListeners unconditionally at the beginning of the
method, before checking audioElement or accessing stream state. Preserve the
existing cleanup and stopping behavior for cases where audioElement is present.
src/app/(app)/map.tsx (1)

183-246: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use the declared Mapbox types and update the interaction check.

Type innerContainerStyle as StyleProp<ViewStyle>, camera configurations as CameraStop, and the callback as MapState. MapState has no properties.isUserInteraction; use state.gestures.isGestureActive. Otherwise, user panning is not recorded and GPS updates can keep recentering an unlocked map.

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

In `@src/app/`(app)/map.tsx around lines 183 - 246, Update the map component to
use declared Mapbox types: type innerContainerStyle as StyleProp<ViewStyle>,
camera configuration objects as CameraStop, and the relevant callback state as
MapState. In the interaction handling, replace
state.properties.isUserInteraction with state.gestures.isGestureActive so user
panning correctly updates the map-moved state and prevents recentering while
unlocked.

Source: Coding guidelines

🧹 Nitpick comments (9)
src/hooks/__tests__/use-signalr-lifecycle.test.tsx (1)

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

Type the SignalR selector mock.

Replace both any usages with a typed mock state and selector. Since SignalRState is private, export it for a Pick or define a local type containing the six selected hub actions and connection fields. Remove the as any cast.

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

In `@src/hooks/__tests__/use-signalr-lifecycle.test.tsx` around lines 48 - 59,
Type the signalRStoreState fixture and mockUseSignalRStore selector using a
local type or exported SignalRState Pick containing the six hub actions and
connection fields. Replace both any usages, type the selector parameter and
return value consistently, and remove the as any cast while preserving the
existing mock behavior.

Source: Coding guidelines

src/__tests__/app/calls.test.tsx (1)

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

Remove the any casts from the mocked store setup.

The casts at Line 70 and Line 79 disable type checking while attaching getState. Use Object.assign or an explicit callable intersection type instead.

Proposed type-safe replacement
-  (useCallsStore as any).getState = jest.fn(() => mockCallsStore);
+  Object.assign(useCallsStore, { getState: jest.fn(() => mockCallsStore) });

-  (securityStore as any).getState = jest.fn(() => mockSecurityStore);
+  Object.assign(securityStore, { getState: jest.fn(() => mockSecurityStore) });

Verify the inferred mock type with the repository's TypeScript check. As per coding guidelines, **/*.{ts,tsx} requires type-safe TypeScript and says to avoid any.

Also applies to: 79-79

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

In `@src/__tests__/app/calls.test.tsx` at line 70, Remove the any casts from the
mocked useCallsStore setup at both getState assignments, using Object.assign or
an explicit callable intersection type so getState remains type-checked while
assigning mockCallsStore. Run the repository’s TypeScript check to verify the
inferred mock type.

Source: Coding guidelines

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

78-85: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Extract the row press handler into a memoized item component.

Line 80 creates a new onPress function for every rendered row. Use a memoized call-row component with a stable callback.

As per coding guidelines, “Avoid anonymous functions in renderItem or event handlers to prevent re-renders.”

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

In `@src/app/`(app)/calls.tsx around lines 78 - 85, Extract the row rendering
logic from renderCallItem into a memoized call-row component with a stable press
callback that navigates using the item’s CallId. Update renderCallItem to render
this component and pass the required call and priority data, avoiding anonymous
functions in the item renderer and event handler.

Source: Coding guidelines

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

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

Flatten the combined view styles.

Lines 53, 61, and 65 pass style arrays directly. Use StyleSheet.flatten() for each combined style before passing it to the React Native view.

As per coding guidelines, “When combining styles, always use StyleSheet.flatten() to merge them into a single object for web platform compatibility.”

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

In `@src/app/`(app)/map.tsx around lines 53 - 70, Use StyleSheet.flatten() for
every combined style array in the marker rendering JSX: the outer marker
container transform style, the markerInnerContainer style combining
markerInnerContainer and innerContainerStyle, and the directionIndicator
transform style. Pass each flattened object to its corresponding View while
preserving the existing style values.

Source: Coding guidelines

src/lib/auth/token-refresh.ts (1)

85-92: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Clear the in-flight marker inside the async operation.

operation.finally(...) creates a new promise that nobody observes. If handlers.onRefreshFailed() throws, operation rejects, and that detached promise becomes an unhandled rejection. Moving the cleanup into a finally block inside the IIFE removes the detached promise and keeps the reset guaranteed.

♻️ Proposed refactor
-  const operation = (async (): Promise<boolean> => {
+  let operation!: Promise<boolean>;
+  operation = (async (): Promise<boolean> => {
     if (!handlers) {
       logger.error({ message: 'Token refresh attempted before initTokenRefresh was called' });
       return false;
     }
@@
-    } catch (error) {
+    } catch (error) {
       logger.error({
         message: 'Token refresh failed',
         context: { error: error instanceof Error ? error.message : String(error) },
       });
       handlers.onRefreshFailed();
       return false;
+    } finally {
+      if (inFlightRefresh === operation) {
+        inFlightRefresh = null;
+      }
     }
   })();
 
   inFlightRefresh = operation;
-  operation.finally(() => {
-    if (inFlightRefresh === operation) {
-      inFlightRefresh = null;
-    }
-  });
-
   return operation;

The early return false paths also need the same reset, so wrap the whole body in try { ... } finally { ... } rather than only the request block.

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

In `@src/lib/auth/token-refresh.ts` around lines 85 - 92, Move the in-flight
marker cleanup into the async IIFE that creates operation, wrapping its entire
body—including early return false paths—in try/finally. In the finally block,
clear inFlightRefresh only when it still equals operation, and remove the
detached operation.finally call while preserving the existing return behavior.
src/hooks/use-saml-login.ts (1)

29-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the redundant manual JSON round-trip.

setItem in src/lib/storage/index.tsx already serializes the value, and getItem already parses it. The current code stringifies twice and parses twice. The values round-trip correctly today, so this is not a defect. Typing the helpers directly makes the contract clearer and removes the dependency on the double encoding.

♻️ Proposed refactor
 async function savePendingState(state: SamlPendingState | null): Promise<void> {
   if (state) {
-    await setItem(SAML_PENDING_STATE_KEY, JSON.stringify(state));
+    await setItem<SamlPendingState>(SAML_PENDING_STATE_KEY, state);
   } else {
     await removeItem(SAML_PENDING_STATE_KEY);
   }
 }
 
 function readPendingState(): SamlPendingState | null {
-  const raw = getItem<string>(SAML_PENDING_STATE_KEY);
-  if (!raw) return null;
-  try {
-    return JSON.parse(raw) as SamlPendingState;
-  } catch {
-    return null;
-  }
+  const state = getItem<SamlPendingState>(SAML_PENDING_STATE_KEY);
+  return state && typeof state.nonce === 'string' && typeof state.startedAt === 'number' ? state : null;
 }

Note: if you apply this refactor, an already persisted double-encoded value parses to a string and is rejected by the type check. That only forces one extra login start.

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

In `@src/hooks/use-saml-login.ts` around lines 29 - 45, Update savePendingState
and readPendingState to rely directly on setItem and getItem serialization,
removing the explicit JSON.stringify and JSON.parse calls. Type the storage
helper usage for SamlPendingState, and validate the retrieved value before
returning it so legacy double-encoded strings are treated as null.
src/services/push-notification.ts (1)

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

Remove the unchecked Href assertion.

as unknown as Href suppresses route validation. Regenerate or correct the typed-route declaration, then pass a typed chat route object. Otherwise, a route refactor can break deep links without a compile-time error.

As per coding guidelines, “Write concise, type-safe TypeScript code.”

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

In `@src/services/push-notification.ts` around lines 35 - 37, Remove the double
cast from the routerPushWithRetry call and correct or regenerate the typed-route
union so the '/chat/[channelId]' route with its channelId parameter is
recognized as a valid Href. Pass the chat route object directly while preserving
the existing retry options and error handling.

Source: Coding guidelines

src/components/incident-command/command-board-view.tsx (1)

73-82: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Stabilize the new press handlers.

The added inline onPress functions create new handler identities on every command-board render. Extract memoized row components or callbacks for channel and direct-message actions.

As per coding guidelines, “Avoid anonymous functions in renderItem or event handlers to prevent re-renders.”

Also applies to: 538-558

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

In `@src/components/incident-command/command-board-view.tsx` around lines 73 - 82,
Stabilize the inline onPress handlers in the command-board row component and the
corresponding direct-message actions around the referenced section by extracting
memoized callbacks or memoized row components. Preserve the existing arguments,
disabled behavior, and onOpen action while ensuring handler identities remain
stable across renders.

Source: Coding guidelines

src/stores/chat/store.ts (1)

265-267: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Avoid downloading the full channel archive for one incident.

chatApi.getChannels(undefined, true) retrieves all active and archived chat channels, then filters them locally by CallId. Add a call-scoped or paginated chat API, or use a bounded cache for incident chat channels. The existing getChannelsForCall endpoint returns voice-channel records and cannot replace this request.

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

In `@src/stores/chat/store.ts` around lines 265 - 267, Update the channel-loading
flow containing getChannels(undefined, true) so it does not download the full
active and archived channel archive before filtering by CallId. Use a
call-scoped or paginated chat-channel API, or a bounded incident-channel cache,
while preserving the incidentChannelsByCallId update; do not substitute the
existing getChannelsForCall endpoint because it returns voice-channel records.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/app/`(app)/_layout.tsx:
- Around line 435-437: Reset hasInitialized.current in the session-ended branch
immediately before or after calling teardownSignedInSession(), so subsequent
sign-ins in the same process satisfy shouldInitialize and rerun initializeApp.
Apply this to the shared sign-out/unauthorized-user path without changing the
teardown behavior.

In `@src/app/`(app)/map.tsx:
- Line 411: Update showRecenterButton to also require a location-presence
boolean derived with null checks for latitude and longitude, preserving zero
coordinates as valid; keep the existing isMapLocked and hasUserMovedMap
conditions.

In `@src/components/incident-command/command-board-view.tsx`:
- Around line 82-83: Remove the isDisabled={!channelId} restriction from the
Button in the channel row so it remains pressable without a channelId. Preserve
the existing onPress handler calling onOpen(channelId, unavailableMessage),
allowing the unavailable feedback path to execute while keeping the current
label behavior unchanged.
- Line 511: Update the icon render sites in
src/components/incident-command/command-board-view.tsx at lines 511, 523, 539,
and 555 and src/components/incident-command/incident-command-tab.tsx at line 57;
also apply the same change to incident-command-tab.tsx line 92 as requested.
Render Lucide components directly inside styled View wrappers, moving icon
classes from the icons or ButtonIcon as props onto the wrappers and replacing
ButtonIcon as usage. Use the existing component patterns and preserve each
icon’s appearance and behavior.

In `@src/hooks/use-direct-message.ts`:
- Line 35: Update the navigation call in the direct-message hook to use the
typed router.push object form with the /chat/[channelId] pathname and channelId
params, replacing the dynamic template-string route while preserving the
existing destination.

In `@src/hooks/use-saml-login.ts`:
- Around line 56-60: Update the URL construction in the SAML login flow around
savePendingState so any existing RelayState query parameter is replaced with the
generated nonce rather than duplicated. Ensure callback parsing and validation
in the corresponding login handler accepts Linking.parse values that may be
string arrays, while still validating the generated nonce correctly.

In `@src/lib/auth/token-refresh.ts`:
- Around line 37-45: Update scheduleTokenRefresh so short-lived tokens do not
trigger a perpetual minimum-delay refresh loop. Confirm the identity server’s
token lifetime contract; if lifetimes can approach REFRESH_BUFFER_MS, derive the
refresh delay from expiresInSeconds (for example, refreshing no earlier than
half the token lifetime) while retaining MIN_REFRESH_DELAY_MS as the lower
bound.

In `@src/stores/app/livekit-store.ts`:
- Around line 220-225: Extract the web audio element cleanup logic from
disconnectFromRoom into a shared helper, then invoke that helper for currentRoom
before removeAllListeners() and disconnect() in the reconnect path. Ensure the
helper removes the mapped audio elements from the document and clears
webAudioElements so resources from the old room are released.
- Around line 402-420: In src/stores/app/livekit-store.ts lines 402-420, move
the cleanup operations and disconnected-state set call into a finally block so
currentRoom, connection flags, and isTalking are reset even if listener removal,
disconnect, or audio playback rejects. In src/stores/app/livekit-store.ts line
376, update the failed-connect reset to also set isTalking to false.

In `@src/stores/auth/store.tsx`:
- Around line 249-269: The applyAuthResponse handler in initTokenRefresh must
not restore authentication after logout has signed the store out. Before
applying response tokens and setting status to signedIn, verify the current
useAuthStore state is not signedOut; reject or otherwise ignore the refresh
response when it is signed out, while preserving normal token updates for active
sessions.

In `@src/translations/ar.json`:
- Line 791: Update the form.invalid_url translation in
src/translations/ar.json:791-791, src/translations/de.json:791-791,
src/translations/es.json:791-791, src/translations/fr.json:791-791,
src/translations/it.json:791-791, and src/translations/pl.json:791-791 so each
locale states that https:// is required and http:// is allowed only for
localhost.
- Line 815: Translate the shared incident-command and authorization strings
currently retaining English values in all six catalogs:
src/translations/ar.json, src/translations/de.json, src/translations/es.json,
src/translations/fr.json, src/translations/it.json, and
src/translations/pl.json. Update the entries at lines 815, 819-821, 828-832,
844-846, 872-873, 877, 896, and 956, covering incident chat states, channel
messages, direct-message errors, authorization messages, open_chat,
send_message, and login.dispatch_not_authorized, while preserving the existing
keys and placeholders.

In `@src/translations/sv.json`:
- Around line 815-846: Translate every specified English user-facing value in
the Swedish and Ukrainian locale catalogs, covering incident-command channels,
chat and direct-message labels, authorization and open_chat entries,
send_message, and login.dispatch_not_authorized. Update src/translations/sv.json
ranges 815-846, 872-877, 896, and 956 with Swedish text, and the corresponding
ranges in src/translations/uk.json with Ukrainian text; preserve all translation
keys and JSON structure.
- Around line 486-489: Update the Swedish translation entries warning_count and
summary to use the locale’s count-aware pluralization convention, preserving the
existing interpolation keys and singular wording for a count of one while using
the correct plural form for counts greater than one.
- Line 837: Correct the Swedish wording of the establish_description translation
so it is grammatically valid, while preserving its intended meaning: creating an
optional leaderboard from a template.

---

Outside diff comments:
In `@src/app/`(app)/map.tsx:
- Around line 183-246: Update the map component to use declared Mapbox types:
type innerContainerStyle as StyleProp<ViewStyle>, camera configuration objects
as CameraStop, and the relevant callback state as MapState. In the interaction
handling, replace state.properties.isUserInteraction with
state.gestures.isGestureActive so user panning correctly updates the map-moved
state and prevents recentering while unlocked.

In `@src/stores/app/audio-stream-store.web.ts`:
- Around line 211-216: Update stopStream to call detachStreamListeners
unconditionally at the beginning of the method, before checking audioElement or
accessing stream state. Preserve the existing cleanup and stopping behavior for
cases where audioElement is present.

---

Nitpick comments:
In `@src/__tests__/app/calls.test.tsx`:
- Line 70: Remove the any casts from the mocked useCallsStore setup at both
getState assignments, using Object.assign or an explicit callable intersection
type so getState remains type-checked while assigning mockCallsStore. Run the
repository’s TypeScript check to verify the inferred mock type.

In `@src/app/`(app)/calls.tsx:
- Around line 78-85: Extract the row rendering logic from renderCallItem into a
memoized call-row component with a stable press callback that navigates using
the item’s CallId. Update renderCallItem to render this component and pass the
required call and priority data, avoiding anonymous functions in the item
renderer and event handler.

In `@src/app/`(app)/map.tsx:
- Around line 53-70: Use StyleSheet.flatten() for every combined style array in
the marker rendering JSX: the outer marker container transform style, the
markerInnerContainer style combining markerInnerContainer and
innerContainerStyle, and the directionIndicator transform style. Pass each
flattened object to its corresponding View while preserving the existing style
values.

In `@src/components/incident-command/command-board-view.tsx`:
- Around line 73-82: Stabilize the inline onPress handlers in the command-board
row component and the corresponding direct-message actions around the referenced
section by extracting memoized callbacks or memoized row components. Preserve
the existing arguments, disabled behavior, and onOpen action while ensuring
handler identities remain stable across renders.

In `@src/hooks/__tests__/use-signalr-lifecycle.test.tsx`:
- Around line 48-59: Type the signalRStoreState fixture and mockUseSignalRStore
selector using a local type or exported SignalRState Pick containing the six hub
actions and connection fields. Replace both any usages, type the selector
parameter and return value consistently, and remove the as any cast while
preserving the existing mock behavior.

In `@src/hooks/use-saml-login.ts`:
- Around line 29-45: Update savePendingState and readPendingState to rely
directly on setItem and getItem serialization, removing the explicit
JSON.stringify and JSON.parse calls. Type the storage helper usage for
SamlPendingState, and validate the retrieved value before returning it so legacy
double-encoded strings are treated as null.

In `@src/lib/auth/token-refresh.ts`:
- Around line 85-92: Move the in-flight marker cleanup into the async IIFE that
creates operation, wrapping its entire body—including early return false
paths—in try/finally. In the finally block, clear inFlightRefresh only when it
still equals operation, and remove the detached operation.finally call while
preserving the existing return behavior.

In `@src/services/push-notification.ts`:
- Around line 35-37: Remove the double cast from the routerPushWithRetry call
and correct or regenerate the typed-route union so the '/chat/[channelId]' route
with its channelId parameter is recognized as a valid Href. Pass the chat route
object directly while preserving the existing retry options and error handling.

In `@src/stores/chat/store.ts`:
- Around line 265-267: Update the channel-loading flow containing
getChannels(undefined, true) so it does not download the full active and
archived channel archive before filtering by CallId. Use a call-scoped or
paginated chat-channel API, or a bounded incident-channel cache, while
preserving the incidentChannelsByCallId update; do not substitute the existing
getChannelsForCall endpoint because it returns voice-channel records.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2c1f8898-5fac-465e-86d3-b7fcb3002d3c

📥 Commits

Reviewing files that changed from the base of the PR and between df24b8b and d7a1303.

📒 Files selected for processing (40)
  • src/__tests__/app/call/[id].test.tsx
  • src/__tests__/app/calls.test.tsx
  • src/__tests__/security-integration.test.ts
  • src/api/chat/chat.ts
  • src/api/common/client.tsx
  • src/app/(app)/_layout.tsx
  • src/app/(app)/calls.tsx
  • src/app/(app)/chatbot.tsx
  • src/app/(app)/home.tsx
  • src/app/(app)/map.tsx
  • src/components/checkIn/check-in-bottom-sheet.tsx
  • src/components/incident-command/command-board-view.tsx
  • src/components/incident-command/incident-command-tab.tsx
  • src/components/pois/poi-detail-screen.tsx
  • src/components/sidebar/call-sidebar.tsx
  • src/hooks/__tests__/use-signalr-lifecycle.test.tsx
  • src/hooks/use-direct-message.ts
  • src/hooks/use-saml-login.ts
  • src/hooks/use-signalr-lifecycle.ts
  • src/lib/__tests__/navigation.test.ts
  • src/lib/auth/token-refresh.ts
  • src/models/v4/chat/chatEnums.ts
  • src/models/v4/security/departmentRightsResultData.ts
  • src/services/push-notification.ts
  • src/services/signalr.service.ts
  • src/stores/app/audio-stream-store.web.ts
  • src/stores/app/livekit-store.ts
  • src/stores/auth/store.tsx
  • src/stores/chat/store.ts
  • src/stores/security/__tests__/store.test.ts
  • src/stores/security/store.ts
  • src/translations/ar.json
  • src/translations/de.json
  • src/translations/en.json
  • src/translations/es.json
  • src/translations/fr.json
  • src/translations/it.json
  • src/translations/pl.json
  • src/translations/sv.json
  • src/translations/uk.json

Comment thread src/app/(app)/_layout.tsx
Comment thread src/app/(app)/map.tsx Outdated
Comment on lines +82 to +83
<Button variant="link" size="xs" onPress={() => onOpen(channelId, unavailableMessage)} isDisabled={!channelId}>
<ButtonText className="text-xs">{channelId ? openLabel : '—'}</ButtonText>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep unavailable channel feedback reachable.

isDisabled={!channelId} prevents Line 82 from calling openChannel. The unavailable toast at Lines 148-151 is therefore unreachable. Keep the row pressable when no channel exists so it can explain the unavailable state.

Proposed fix
-    <Button variant="link" size="xs" onPress={() => onOpen(channelId, unavailableMessage)} isDisabled={!channelId}>
+    <Button variant="link" size="xs" onPress={() => onOpen(channelId, unavailableMessage)}>
📝 Committable suggestion

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

Suggested change
<Button variant="link" size="xs" onPress={() => onOpen(channelId, unavailableMessage)} isDisabled={!channelId}>
<ButtonText className="text-xs">{channelId ? openLabel : '—'}</ButtonText>
<Button variant="link" size="xs" onPress={() => onOpen(channelId, unavailableMessage)}>
<ButtonText className="text-xs">{channelId ? openLabel : '—'}</ButtonText>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/incident-command/command-board-view.tsx` around lines 82 - 83,
Remove the isDisabled={!channelId} restriction from the Button in the channel
row so it remains pressable without a channelId. Preserve the existing onPress
handler calling onOpen(channelId, unavailableMessage), allowing the unavailable
feedback path to execute while keeping the current label behavior unchanged.

<ChatChannelRow
label={t('incident_command.incident_channel')}
hint={t('incident_command.incident_channel_hint')}
icon={<MessagesSquareIcon size={16} className="text-blue-500" />}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- file map ---'
ast-grep outline src/components/incident-command/command-board-view.tsx
ast-grep outline src/components/incident-command/incident-command-tab.tsx
printf '%s\n' '--- focused source ---'
sed -n '1,90p' src/components/incident-command/incident-command-tab.tsx
sed -n '470,575p' src/components/incident-command/command-board-view.tsx
printf '%s\n' '--- relevant symbols and wrapper patterns ---'
rg -n -C 3 'MessagesSquareIcon|RadioIcon|MessageCircleIcon|NetworkIcon|ButtonIcon|lucide-react-native|StyleSheet\.flatten|<View' \
  src/components/incident-command/command-board-view.tsx \
  src/components/incident-command/incident-command-tab.tsx
printf '%s\n' '--- package/config references ---'
rg -n -C 2 'lucide-react-native|ButtonIcon' package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || true

Repository: Resgrid/Dispatch

Length of output: 29751


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- changed files and focused diff ---'
git status --short
git diff --stat
git diff -- src/components/incident-command/command-board-view.tsx src/components/incident-command/incident-command-tab.tsx

printf '%s\n' '--- UI component locations ---'
fd -i 'button|view|hstack|vstack' src/components/ui -t f
printf '%s\n' '--- Button implementation ---'
BUTTON_FILE="$(fd -i -t f 'button' src/components/ui | head -n 1)"
if [ -n "$BUTTON_FILE" ]; then
  ast-grep outline "$BUTTON_FILE"
  cat -n "$BUTTON_FILE"
fi
printf '%s\n' '--- View and layout imports/usages ---'
rg -n -C 3 "from 'react-native'|from '`@/components/ui/view`'|<View|className=.*mr-1|className=.*text-blue-500" \
  src/components/incident-command src/components/ui | head -n 300
printf '%s\n' '--- all affected icon occurrences ---'
rg -n -C 2 'NetworkIcon|MessagesSquareIcon|RadioIcon|MessageCircleIcon' src/components/incident-command

Repository: Resgrid/Dispatch

Length of output: 32909


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- tracked UI files ---'
git ls-files src/components/ui | rg '(^|/)(button|box|hstack|vstack)(/|\.|$)' || true
printf '%s\n' '--- ButtonIcon declarations and exports ---'
rg -n -C 8 'ButtonIcon|createButton|buttonIcon|Icon' src/components/ui src | head -n 500
printf '%s\n' '--- relevant component files ---'
for f in $(git ls-files src/components/ui | rg 'button'); do
  echo "--- $f"
  wc -l "$f"
  sed -n '1,240p' "$f"
done
printf '%s\n' '--- direct lucide styling and wrapper conventions ---'
rg -n -C 2 '<(NetworkIcon|MessagesSquareIcon|RadioIcon|MessageCircleIcon|[A-Za-z]+Icon)[^>]*(className|style)=|<(Box|View)[^>]*className="[^"]*(text-|mr-|ml-)' src | head -n 400

Repository: Resgrid/Dispatch

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- Button implementation tail ---'
sed -n '220,340p' src/components/ui/button/index.tsx
printf '%s\n' '--- focused JSX and imports ---'
sed -n '1,75p' src/components/incident-command/command-board-view.tsx
sed -n '35,110p' src/components/incident-command/incident-command-tab.tsx
printf '%s\n' '--- compact wrapper examples around icons ---'
rg -n -C 4 '<(Box|View)[^>]*>[^<]*<(MessagesSquareIcon|RadioIcon|MessageCircleIcon|NetworkIcon)|<(Box|View)[^>]*className="[^"]*(mr-1|text-blue-500|text-gray-400)' src/components src/app -g '*.tsx' | head -n 160

Repository: Resgrid/Dispatch

Length of output: 9934


🏁 Script executed:

#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
import re

files = [
    Path("src/components/incident-command/command-board-view.tsx"),
    Path("src/components/incident-command/incident-command-tab.tsx"),
]
direct_styled = re.compile(r"<([A-Za-z]+Icon)\b[^>]*\bclassName=")
button_icon = re.compile(r"<ButtonIcon\b[^>]*\bas=\{([A-Za-z]+Icon)\}")

for path in files:
    print(path)
    for number, line in enumerate(path.read_text().splitlines(), 1):
        direct = direct_styled.search(line)
        button = button_icon.search(line)
        if direct:
            print(f"  direct styled Lucide: line {number}: {direct.group(1)}")
        if button:
            print(f"  ButtonIcon-wrapped Lucide: line {number}: {button.group(1)}")
PY

Repository: Resgrid/Dispatch

Length of output: 1418


Render Lucide icons through styled View wrappers.

Move icon classes to wrappers and render Lucide components directly. Replace ButtonIcon as={...} at command-board-view.tsx lines 539 and 555, and incident-command-tab.tsx lines 79 and 98. Apply the same wrapper to direct styled icons at lines 511, 523, 57, and 92.

📍 Affects 2 files
  • src/components/incident-command/command-board-view.tsx#L511-L511 (this comment)
  • src/components/incident-command/command-board-view.tsx#L523-L523
  • src/components/incident-command/command-board-view.tsx#L539-L539
  • src/components/incident-command/command-board-view.tsx#L555-L555
  • src/components/incident-command/incident-command-tab.tsx#L57-L57
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/incident-command/command-board-view.tsx` at line 511, Update
the icon render sites in src/components/incident-command/command-board-view.tsx
at lines 511, 523, 539, and 555 and
src/components/incident-command/incident-command-tab.tsx at line 57; also apply
the same change to incident-command-tab.tsx line 92 as requested. Render Lucide
components directly inside styled View wrappers, moving icon classes from the
icons or ButtonIcon as props onto the wrappers and replacing ButtonIcon as
usage. Use the existing component patterns and preserve each icon’s appearance
and behavior.

Source: Coding guidelines

Comment thread src/hooks/use-direct-message.ts Outdated
Comment thread src/translations/ar.json
"table_address": "العنوان",
"table_scheduled": "مجدول في"
"form": {
"invalid_url": "يرجى إدخال عنوان URL صالح يبدأ بـ http:// أو https://",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep form.invalid_url consistent across all locales.

The English catalog now states that https:// is required and http:// is allowed only for localhost. The six non-English catalogs still state that both schemes are valid.

  • src/translations/ar.json#L791-L791: update the Arabic validation message.
  • src/translations/de.json#L791-L791: update the German validation message.
  • src/translations/es.json#L791-L791: update the Spanish validation message.
  • src/translations/fr.json#L791-L791: update the French validation message.
  • src/translations/it.json#L791-L791: update the Italian validation message.
  • src/translations/pl.json#L791-L791: update the Polish validation message.
📍 Affects 6 files
  • src/translations/ar.json#L791-L791 (this comment)
  • src/translations/de.json#L791-L791
  • src/translations/es.json#L791-L791
  • src/translations/fr.json#L791-L791
  • src/translations/it.json#L791-L791
  • src/translations/pl.json#L791-L791
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/translations/ar.json` at line 791, Update the form.invalid_url
translation in src/translations/ar.json:791-791,
src/translations/de.json:791-791, src/translations/es.json:791-791,
src/translations/fr.json:791-791, src/translations/it.json:791-791, and
src/translations/pl.json:791-791 so each locale states that https:// is required
and http:// is allowed only for localhost.

Comment thread src/translations/ar.json
"call": "البلاغ",
"channel_name": "اسم القناة",
"chat": "Chat",
"chat_frozen": "This incident is closed. Conversations are kept as a point-in-time record — no new messages.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

Localize the shared incident-command and authorization strings.

All six non-English catalogs repeat English values in the incident-command chat states, channel messages, direct-message errors, authorization messages, open_chat, send_message, and login.dispatch_not_authorized.

  • src/translations/ar.json#L815-L815: translate the English values at Lines 815, 819-821, 828-832, 844-846, 872-873, 877, 896, and 956.
  • src/translations/de.json#L815-L815: translate the English values at Lines 815, 819-821, 828-832, 844-846, 872-873, 877, 896, and 956.
  • src/translations/es.json#L815-L815: translate the English values at Lines 815, 819-821, 828-832, 844-846, 872-873, 877, 896, and 956.
  • src/translations/fr.json#L815-L815: translate the English values at Lines 815, 819-821, 828-832, 844-846, 872-873, 877, 896, and 956.
  • src/translations/it.json#L815-L815: translate the English values at Lines 815, 819-821, 828-832, 844-846, 872-873, 877, 896, and 956.
  • src/translations/pl.json#L815-L815: translate the English values at Lines 815, 819-821, 828-832, 844-846, 872-873, 877, 896, and 956.
📍 Affects 6 files
  • src/translations/ar.json#L815-L815 (this comment)
  • src/translations/de.json#L815-L815
  • src/translations/es.json#L815-L815
  • src/translations/fr.json#L815-L815
  • src/translations/it.json#L815-L815
  • src/translations/pl.json#L815-L815
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/translations/ar.json` at line 815, Translate the shared incident-command
and authorization strings currently retaining English values in all six
catalogs: src/translations/ar.json, src/translations/de.json,
src/translations/es.json, src/translations/fr.json, src/translations/it.json,
and src/translations/pl.json. Update the entries at lines 815, 819-821, 828-832,
844-846, 872-873, 877, 896, and 956, covering incident chat states, channel
messages, direct-message errors, authorization messages, open_chat,
send_message, and login.dispatch_not_authorized, while preserving the existing
keys and placeholders.

Comment thread src/translations/sv.json
Comment on lines +486 to +489
"warning_count": "{{count}} varning",
"enable_timers": "Aktivera timrar",
"disable_timers": "Inaktivera timrar",
"summary": "{{overdue}} försenade, {{warning}} varning, {{ok}} ok",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add count-aware Swedish pluralization.

warning_count and summary always use singular varning after interpolated counts. The UI displays incorrect Swedish when more than one warning exists. Use the locale's pluralization convention for these messages.

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

In `@src/translations/sv.json` around lines 486 - 489, Update the Swedish
translation entries warning_count and summary to use the locale’s count-aware
pluralization convention, preserving the existing interpolation keys and
singular wording for a count of one while using the correct plural form for
counts greater than one.

Comment thread src/translations/sv.json
Comment on lines +815 to +846
"chat_frozen": "This incident is closed. Conversations are kept as a point-in-time record — no new messages.",
"close_all_channels": "Stäng alla kanaler",
"close_command": "Avsluta ledning",
"closed": "Avslutad",
"command_channel": "Command chat",
"command_channel_hint": "Command staff and dispatch",
"command_channel_unavailable": "No command channel has been created for this incident yet.",
"commander": "Insatsledare",
"complete": "Slutför",
"completed": "Slutförd",
"confirm_close": "Avsluta insatsledningen för det här larmet?",
"critical": "Kritisk",
"delete_annotation_confirm": "Ta bort den här anteckningen?",
"dispatch_channel": "Dispatch",
"dispatch_channel_hint": "The incident's line to the desk",
"dispatch_channel_unavailable": "No dispatch channel has been created for this incident yet.",
"dm_failed": "Couldn't open that conversation.",
"dm_unavailable": "That contact has no Resgrid account to message.",
"due": "Förfaller",
"edit": "Redigera",
"edit_action_plan": "Redigera insatsplan",
"establish": "Upprätta ledning",
"establish_description": "Skapa valfritt ledningstavlan från en mall.",
"establish_error": "Det gick inte att upprätta ledningen",
"establish_success": "Insatsledningen har upprättats",
"establish_title": "Upprätta insatsledning",
"established_on": "Upprättad",
"green": "Grön",
"hold_to_talk": "Håll in för att tala",
"incident_channel": "Incident chat",
"incident_channel_hint": "Everyone working the incident",
"incident_channel_unavailable": "No incident channel has been created for this call yet.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Complete the translations for incident-command and login flows.

Both locale catalogs contain English user-facing values in these ranges. This creates mixed-language UI during chat, authorization, direct-message, and login flows.

  • src/translations/sv.json#L815-L846: Translate the English incident-command channel, chat, and direct-message values into Swedish.
  • src/translations/sv.json#L872-L877: Translate the authorization and open_chat values into Swedish.
  • src/translations/sv.json#L896-L896: Translate send_message into Swedish.
  • src/translations/sv.json#L956-L956: Translate login.dispatch_not_authorized into Swedish.
  • src/translations/uk.json#L815-L846: Translate the English incident-command channel, chat, and direct-message values into Ukrainian.
  • src/translations/uk.json#L872-L877: Translate the authorization and open_chat values into Ukrainian.
  • src/translations/uk.json#L896-L896: Translate send_message into Ukrainian.
  • src/translations/uk.json#L956-L956: Translate login.dispatch_not_authorized into Ukrainian.
📍 Affects 2 files
  • src/translations/sv.json#L815-L846 (this comment)
  • src/translations/sv.json#L872-L877
  • src/translations/sv.json#L896-L896
  • src/translations/sv.json#L956-L956
  • src/translations/uk.json#L815-L846
  • src/translations/uk.json#L872-L877
  • src/translations/uk.json#L896-L896
  • src/translations/uk.json#L956-L956
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/translations/sv.json` around lines 815 - 846, Translate every specified
English user-facing value in the Swedish and Ukrainian locale catalogs, covering
incident-command channels, chat and direct-message labels, authorization and
open_chat entries, send_message, and login.dispatch_not_authorized. Update
src/translations/sv.json ranges 815-846, 872-877, 896, and 956 with Swedish
text, and the corresponding ranges in src/translations/uk.json with Ukrainian
text; preserve all translation keys and JSON structure.

Comment thread src/translations/sv.json
"edit": "Redigera",
"edit_action_plan": "Redigera insatsplan",
"establish": "Upprätta ledning",
"establish_description": "Skapa valfritt ledningstavlan från en mall.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the Swedish grammar in establish_description.

Skapa valfritt ledningstavlan från en mall. is not grammatical Swedish.

Proposed fix
-    "establish_description": "Skapa valfritt ledningstavlan från en mall.",
+    "establish_description": "Skapa vid behov en ledningstavla från en mall.",
📝 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
"establish_description": "Skapa valfritt ledningstavlan från en mall.",
"establish_description": "Skapa vid behov en ledningstavla från en mall.",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/translations/sv.json` at line 837, Correct the Swedish wording of the
establish_description translation so it is grammatically valid, while preserving
its intended meaning: creating an optional leaderboard from a template.

Comment thread src/api/chat/chat.ts
Comment on lines 47 to 50
const response = await api.get<ChatV4Response<ChatChannelResultData[]>>(`${CHAT}/GetChannels`, {
params: activeUnitId != null ? { activeUnitId } : undefined,
params: Object.keys(params).length > 0 ? params : undefined,
signal,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Uncaught network exception: The external HTTP call api.get lacks a try/catch with context mapping, violating rule [27]. Wrap the call to map exceptions to application-level errors using AppError.

Kody rule violation: Add try-catch blocks for external calls

Prompt for LLM

File src/api/chat/chat.ts:

Line 47 to 50:

Uncaught network exception: The external HTTP call `api.get` lacks a try/catch with context mapping, violating rule [27]. Wrap the call to map exceptions to application-level errors using `AppError`.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/app/(app)/_layout.tsx
isInitializing.current = false;

// Stop hubs, voice, audio and timers that belong to the ended session
void teardownSignedInSession();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unhandled Promise rejection: Calling teardownSignedInSession() with void discards its Promise, meaning errors thrown before the internal loop (e.g., useSignalRStore.getState() on line 53) violate rule [1]. Attach a .catch() handler or wrap the entire function body in try/catch.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File src/app/(app)/_layout.tsx:

Line 437:

Unhandled Promise rejection: Calling `teardownSignedInSession()` with `void` discards its Promise, meaning errors thrown before the internal loop (e.g., `useSignalRStore.getState()` on line 53) violate rule [1]. Attach a `.catch()` handler or wrap the entire function body in try/catch.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/app/(app)/calls.tsx

const renderCallItem = useCallback(
({ item }: { item: CallResultData }) => (
<Pressable onPress={() => router.push(`/call/${item.CallId}` as Href)}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Performance degradation: Inline arrow functions in JSX props create new functions on every render. Move these function definitions outside the render method.

Kody rule violation: Avoid using .bind() or arrow functions in JSX props

Prompt for LLM

File src/app/(app)/calls.tsx:

Line 80:

Performance degradation: Inline arrow functions in JSX props create new functions on every render. Move these function definitions outside the render method.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/app/(app)/calls.tsx
.filter((call) => call.CallId.toLowerCase().includes(searchQuery.toLowerCase()) || (call.Nature?.toLowerCase() || '').includes(searchQuery.toLowerCase()));
const filteredCalls = useMemo(() => {
const query = searchQuery.toLowerCase();
return calls.filter((call) => call.State !== CallState.SCHEDULED).filter((call) => call.CallId.toLowerCase().includes(query) || (call.Nature?.toLowerCase() || '').includes(query));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Redundant array iteration: Two chained .filter() calls iterate the calls array twice per render, violating rule 97. Combine both predicates into a single .filter() call to optimize performance.

Kody rule violation: Optimize chained array operations

Prompt for LLM

File src/app/(app)/calls.tsx:

Line 68:

Redundant array iteration: Two chained `.filter()` calls iterate the `calls` array twice per render, violating rule 97. Combine both predicates into a single `.filter()` call to optimize performance.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/app/(app)/map.tsx
centerCoordinate: [location.longitude, location.latitude],
zoomLevel: location.isMapLocked ? 16 : 12,
centerCoordinate: [longitude, latitude],
zoomLevel: locked ? 16 : 12,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Magic numbers: Inline zoom levels 16 and 12 are repeated across multiple effects and handlers in map.tsx. Define named constants like LOCKED_ZOOM_LEVEL and UNLOCKED_ZOOM_LEVEL at module scope.

Kody rule violation: Replace magic numbers with named constants

Prompt for LLM

File src/app/(app)/map.tsx:

Line 185:

Magic numbers: Inline zoom levels 16 and 12 are repeated across multiple effects and handlers in `map.tsx`. Define named constants like `LOCKED_ZOOM_LEVEL` and `UNLOCKED_ZOOM_LEVEL` at module scope.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/hooks/use-direct-message.ts Outdated
useToastStore.getState().showToast('error', t('incident_command.dm_failed'));
return;
}
router.push(`/chat/${channelId}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Hardcoded route literal: The route path /chat/${channelId} is inlined as a raw string, violating rule 6. Define a centralized routes constant or route-builder function to prevent drift.

Kody rule violation: Centralize string constants

Prompt for LLM

File src/hooks/use-direct-message.ts:

Line 35:

Hardcoded route literal: The route path `/chat/${channelId}` is inlined as a raw string, violating rule 6. Define a centralized routes constant or route-builder function to prevent drift.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

router.push(`/chat/${channelId}`);
} catch (error) {
logger.error({ message: 'chat: failed to open direct message', context: { error, targetUserId } });
useToastStore.getState().showToast('error', t('incident_command.dm_failed'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Magic string: The toast severity 'error' is repeated multiple times, violating rule 8. Replace these string literals with a ToastType enum to prevent typos and drift.

Kody rule violation: Use enums instead of magic strings

Prompt for LLM

File src/hooks/use-direct-message.ts:

Line 38:

Magic string: The toast severity `'error'` is repeated multiple times, violating rule 8. Replace these string literals with a `ToastType` enum to prevent typos and drift.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

if (!raw) return null;
try {
return JSON.parse(raw) as SamlPendingState;
} catch {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Silent error swallowing: The catch block in use-saml-login.ts hides potential JSON.parse security issues like injection attempts by returning null. Log the error with context before returning null to expose corrupted storage states.

Kody rule violation: Avoid empty catch blocks

Prompt for LLM

File src/hooks/use-saml-login.ts:

Line 42:

Silent error swallowing: The catch block in `use-saml-login.ts` hides potential `JSON.parse` security issues like injection attempts by returning null. Log the error with context before returning null to expose corrupted storage states.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

const raw = getItem<string>(SAML_PENDING_STATE_KEY);
if (!raw) return null;
try {
return JSON.parse(raw) as SamlPendingState;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Security bypass risk: Type-asserting JSON.parse output as SamlPendingState allows tampered storage to bypass the SAML_FLOW_MAX_AGE_MS expiry check and weaken CSRF nonce comparisons. Validate the shape of the parsed object to ensure nonce and startedAt exist before returning.

Kody rule violation: Always validate JSON parsing

Prompt for LLM

File src/hooks/use-saml-login.ts:

Line 41:

Security bypass risk: Type-asserting `JSON.parse` output as `SamlPendingState` allows tampered storage to bypass the `SAML_FLOW_MAX_AGE_MS` expiry check and weaken CSRF nonce comparisons. Validate the shape of the parsed object to ensure `nonce` and `startedAt` exist before returning.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/stores/chat/store.ts
Comment on lines +264 to +267
try {
const response = await chatApi.getChannels(undefined, true);
const forCall = (response.Data ?? []).filter((channel) => channel.CallId === numericCallId);
set((state) => ({ incidentChannelsByCallId: { ...state.incidentChannelsByCallId, [callId]: forCall } }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Performance high

Performance bottleneck: loadIncidentChannels fetches all archived department channels via getChannels(undefined, true) and filters client-side, loading hundreds of unnecessary objects per tab open. Pass a callId query param to the API for server-side filtering, or cache results in incidentChannelsByCallId.

// Skip if already loaded for this call
if (get().incidentChannelsByCallId[callId]) return;
// Prefer a server-side filter when available:
// const response = await chatApi.getIncidentChannels(numericCallId);
const response = await chatApi.getChannels(undefined, true);
const forCall = (response.Data ?? []).filter((channel) => channel.CallId === numericCallId);
Prompt for LLM

File src/stores/chat/store.ts:

Line 264 to 267:

Performance bottleneck: `loadIncidentChannels` fetches all archived department channels via `getChannels(undefined, true)` and filters client-side, loading hundreds of unnecessary objects per tab open. Pass a `callId` query param to the API for server-side filtering, or cache results in `incidentChannelsByCallId`.

Suggested Code:

// Skip if already loaded for this call
if (get().incidentChannelsByCallId[callId]) return;
// Prefer a server-side filter when available:
// const response = await chatApi.getIncidentChannels(numericCallId);
const response = await chatApi.getChannels(undefined, true);
const forCall = (response.Data ?? []).filter((channel) => channel.CallId === numericCallId);

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/stores/chat/store.ts
Comment on lines +264 to +267
try {
const response = await chatApi.getChannels(undefined, true);
const forCall = (response.Data ?? []).filter((channel) => channel.CallId === numericCallId);
set((state) => ({ incidentChannelsByCallId: { ...state.incidentChannelsByCallId, [callId]: forCall } }));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Performance high

Network bandwidth waste: loadIncidentChannels fetches every archived department channel via getChannels(undefined, true) and filters client-side, transferring thousands of objects on every tab open. Add an optional callId filter parameter to getChannels or call a dedicated server-side endpoint to reduce network and memory costs.

// Filter server-side by CallId to avoid loading the entire department's channel history
const response = await chatApi.getChannels(undefined, true, undefined, numericCallId);
const forCall = response.Data ?? [];
Prompt for LLM

File src/stores/chat/store.ts:

Line 264 to 267:

Network bandwidth waste: `loadIncidentChannels` fetches every archived department channel via `getChannels(undefined, true)` and filters client-side, transferring thousands of objects on every tab open. Add an optional `callId` filter parameter to `getChannels` or call a dedicated server-side endpoint to reduce network and memory costs.

Suggested Code:

// Filter server-side by CallId to avoid loading the entire department's channel history
const response = await chatApi.getChannels(undefined, true, undefined, numericCallId);
const forCall = response.Data ?? [];

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/stores/chat/store.ts
const forCall = (response.Data ?? []).filter((channel) => channel.CallId === numericCallId);
set((state) => ({ incidentChannelsByCallId: { ...state.incidentChannelsByCallId, [callId]: forCall } }));
} catch (error) {
logger.error({ message: 'chat: failed to load incident channels', context: { error, callId } });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unstructured logging: The operation name is embedded only in the message string, violating rule [3]. Add an explicit operation field like op: 'loadIncidentChannels' in the logger context for searchable, queryable logs.

Kody rule violation: Include error context in structured logs

Prompt for LLM

File src/stores/chat/store.ts:

Line 269:

Unstructured logging: The operation name is embedded only in the message string, violating rule [3]. Add an explicit operation field like `op: 'loadIncidentChannels'` in the logger context for searchable, queryable logs.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Comment thread src/stores/chat/store.ts
// ------------------------------------------------------------------
incidentChannelsByCallId: {},

loadIncidentChannels: async (callId: string) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Missing documentation: The async function loadIncidentChannels lacks JSDoc for its Promise return value and rejection conditions, violating rule [22]. Add a @returns {Promise<Type>} block to document resolve and rejection behavior.

Kody rule violation: Document async/Promise behavior and errors

Prompt for LLM

File src/stores/chat/store.ts:

Line 258:

Missing documentation: The async function `loadIncidentChannels` lacks JSDoc for its Promise return value and rejection conditions, violating rule [22]. Add a `@returns {Promise<Type>}` block to document resolve and rejection behavior.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

canUserCreateMessages: store.rights?.CanCreateMessage,
canUserViewPII: store.rights?.CanViewPII,
// Undefined (rights not loaded yet) is treated as allowed by callers; only an explicit false blocks.
canUserWorkCommand: store.rights?.CanLoginToCommandApp,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Authorization bypass vulnerability: The canUserWorkCommand permission flag returns undefined when store.rights hasn't loaded, causing callers to fail open and grant unauthorized Command App access. Default the derived flag to false using ?? false, and apply this deny-by-default fix to sibling flags like canUserCreateNotes and canUserViewPII.

Kody rule violation: Implement RBAC with least privilege and deny-by-default

Prompt for LLM

File src/stores/security/store.ts:

Line 84:

Authorization bypass vulnerability: The `canUserWorkCommand` permission flag returns `undefined` when `store.rights` hasn't loaded, causing callers to fail open and grant unauthorized Command App access. Default the derived flag to `false` using `?? false`, and apply this deny-by-default fix to sibling flags like `canUserCreateNotes` and `canUserViewPII`.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

@Resgrid-Bot

Resgrid-Bot commented Aug 11, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

The code review was successfully completed based on your current configurations.

Kody Guide: Usage and Configuration
Interacting with Kody
  • Request a Review: Ask Kody to review your PR manually by adding a comment with the @kody start-review command at the root of your PR.

  • Validate Business Logic: Ask Kody to validate your code against business rules by adding a comment with the @kody -v business-logic command.

  • Provide Feedback: Help Kody learn and improve by reacting to its comments with a 👍 for helpful suggestions or a 👎 if improvements are needed.

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

@ucswift

ucswift commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

Approve

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This PR is approved.

@ucswift
ucswift merged commit d44c39c into master Aug 11, 2026
9 of 11 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

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

Inline comments:
In `@src/stores/auth/__tests__/token-refresh-race.test.ts`:
- Around line 18-22: Update the authResponse fixture in
token-refresh-race.test.ts to declare it directly as AuthResponse instead of
using a type assertion, and provide the required id_token, token_type, and
expiration_date fields alongside the existing values.
- Around line 51-55: Update logout and the token-refresh coordination so signing
out invalidates any in-flight refresh before awaiting
clearPasswordVerificationHash, preventing applyAuthResponse from restoring the
session. Revise the race test around logout and clearPasswordVerificationHash to
use a deferred cleanup promise, invoke the real logout flow, resolve the refresh
during cleanup, and assert the signed-out state and false refresh result.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c98143f-f55a-4e33-997f-97a98765b2b1

📥 Commits

Reviewing files that changed from the base of the PR and between d7a1303 and 758cf44.

📒 Files selected for processing (12)
  • src/api/chat/chat.ts
  • src/app/(app)/_layout.tsx
  • src/app/(app)/chat.tsx
  • src/app/(app)/map.tsx
  • src/components/incident-command/command-board-view.tsx
  • src/hooks/use-direct-message.ts
  • src/lib/auth/__tests__/token-refresh.test.ts
  • src/lib/auth/token-refresh.ts
  • src/stores/app/livekit-store.ts
  • src/stores/auth/__tests__/token-refresh-race.test.ts
  • src/stores/auth/store.tsx
  • src/stores/chat/store.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/stores/auth/store.tsx
  • src/hooks/use-direct-message.ts
  • src/app/(app)/_layout.tsx
  • src/stores/app/livekit-store.ts
  • src/stores/chat/store.ts
  • src/app/(app)/map.tsx
  • src/lib/auth/token-refresh.ts
  • src/components/incident-command/command-board-view.tsx

Comment on lines +18 to +22
const authResponse = {
access_token: 'new-access',
refresh_token: 'new-refresh',
expires_in: 3600,
} as AuthResponse;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -A12 -B2 'interface AuthResponse' src/lib/auth/types.tsx
rg -n -A8 -B2 'const authResponse|as AuthResponse' src/stores/auth/__tests__/token-refresh-race.test.ts

Repository: Resgrid/Dispatch

Length of output: 861


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- test file ---'
cat -n src/stores/auth/__tests__/token-refresh-race.test.ts | sed -n '1,180p'

printf '%s\n' '--- AuthResponse usages ---'
rg -n -S 'AuthResponse|id_token|expiration_date|token_type' src/stores src/lib | head -200

printf '%s\n' '--- TypeScript configuration ---'
fd -a -i 'tsconfig*.json' . -x sh -c 'echo "--- $1"; cat "$1"' sh {} \;

printf '%s\n' '--- Static fixture check ---'
python3 - <<'PY'
from pathlib import Path
import re

types = Path("src/lib/auth/types.tsx").read_text()
test = Path("src/stores/auth/__tests__/token-refresh-race.test.ts").read_text()

fields = re.findall(r"^\s+([A-Za-z_]\w*)\s*:", types[types.index("export interface AuthResponse {"):types.index("export interface LoginResponse {")], re.M)
fixture = re.search(r"const authResponse\s*=\s*\{(.*?)\}\s*as AuthResponse", test, re.S)
fixture_fields = re.findall(r"^\s+([A-Za-z_]\w*)\s*:", fixture.group(1), re.M) if fixture else []
print("AuthResponse fields:", fields)
print("Fixture fields:", fixture_fields)
print("Missing fields:", [field for field in fields if field not in fixture_fields])
PY

Repository: Resgrid/Dispatch

Length of output: 7248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- token refresh implementation ---'
cat -n src/lib/auth/token-refresh.ts | sed -n '1,120p'

printf '%s\n' '--- auth response application ---'
cat -n src/stores/auth/store.tsx | sed -n '235,285p'

printf '%s\n' '--- complete AuthResponse type ---'
cat -n src/lib/auth/types.tsx | sed -n '1,25p'

printf '%s\n' '--- Test type-safety probe ---'
python3 - <<'PY'
from pathlib import Path
import re

types = Path("src/lib/auth/types.tsx").read_text()
test = Path("src/stores/auth/__tests__/token-refresh-race.test.ts").read_text()

interface = re.search(r"export interface AuthResponse\s*\{(.*?)\}", types, re.S)
fixture = re.search(r"const authResponse\s*=\s*\{(.*?)\}\s*as AuthResponse", test, re.S)
required = re.findall(r"^\s+([A-Za-z_]\w*)\s*:\s*([^;]+);", interface.group(1), re.M)
provided = re.findall(r"^\s+([A-Za-z_]\w*)\s*:", fixture.group(1), re.M)
print("Required AuthResponse fields:", [name for name, _ in required])
print("Provided fixture fields:", provided)
print("Assertion bypasses missing-field diagnostics:", set(name for name, _ in required) - set(provided))
PY

Repository: Resgrid/Dispatch

Length of output: 6959


Use a complete AuthResponse fixture.

The fixture omits id_token, token_type, and expiration_date. The as AuthResponse assertion suppresses this type error. Declare the fixture as AuthResponse and provide all required fields.

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

In `@src/stores/auth/__tests__/token-refresh-race.test.ts` around lines 18 - 22,
Update the authResponse fixture in token-refresh-race.test.ts to declare it
directly as AuthResponse instead of using a type assertion, and provide the
required id_token, token_type, and expiration_date fields alongside the existing
values.

Source: Coding guidelines

Comment on lines +51 to +55
// Logout wins the race: store cleared and signed out
useAuthStore.setState({ status: 'signedOut', accessToken: null, refreshToken: null, profile: null, userId: null });

resolveRefresh(authResponse);
await expect(refreshPromise).resolves.toBe(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/stores/auth/store.tsx --items all
rg -n -C4 --glob '*.{ts,tsx}' 'performTokenRefresh|initTokenRefresh|applyAuthResponse|signOut|logout' src/stores/auth src/lib/auth

Repository: Resgrid/Dispatch

Length of output: 12144


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- auth store logout implementation ---'
sed -n '110,170p' src/stores/auth/store.tsx

printf '%s\n' '--- token refresh implementation ---'
sed -n '1,130p' src/lib/auth/token-refresh.ts

printf '%s\n' '--- race test ---'
cat -n src/stores/auth/__tests__/token-refresh-race.test.ts

Repository: Resgrid/Dispatch

Length of output: 7980


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- cleanup implementation and response type ---'
rg -n -C6 'clearPasswordVerificationHash|interface AuthResponse|type AuthResponse' src/lib src/stores

printf '%s\n' '--- deterministic source-order check ---'
python3 - <<'PY'
from pathlib import Path

store = Path("src/stores/auth/store.tsx").read_text()
logout_start = store.index("logout: async () =>")
logout_end = store.index("refreshAccessToken: async () =>", logout_start)
logout = store[logout_start:logout_end]

cleanup = logout.index("await clearPasswordVerificationHash();")
signed_out = logout.index("status: 'signedOut'")
print({
    "cleanup_is_awaited": "await clearPasswordVerificationHash();" in logout,
    "signed_out_assignment_after_cleanup": signed_out > cleanup,
    "timer_cancelled_before_cleanup": logout.index("cancelScheduledTokenRefresh();") < cleanup,
})
PY

Repository: Resgrid/Dispatch

Length of output: 5879


Make sign-out invalidate the in-flight refresh.

logout() awaits clearPasswordVerificationHash() before setting status to signedOut. If the refresh resolves during cleanup, applyAuthResponse can restore the session. Set the signed-out state before cleanup or add explicit refresh invalidation. Test this path with a deferred cleanup promise instead of calling setState directly.

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

In `@src/stores/auth/__tests__/token-refresh-race.test.ts` around lines 51 - 55,
Update logout and the token-refresh coordination so signing out invalidates any
in-flight refresh before awaiting clearPasswordVerificationHash, preventing
applyAuthResponse from restoring the session. Revise the race test around logout
and clearPasswordVerificationHash to use a deferred cleanup promise, invoke the
real logout flow, resolve the refresh during cleanup, and assert the signed-out
state and false refresh result.

await currentRoom.disconnect();
await audioService.playDisconnectedFromAudioRoomSound();
currentRoom.removeAllListeners();
await currentRoom.disconnect();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unhandled rejection occurs in src/stores/app/livekit-store.ts:413 and 417 because the currentRoom.disconnect() external network call is wrapped only in try/finally, violating Rule 27 by propagating teardown failures uncaught and unlogged. Add a catch block to log structured context (e.g., room SID, currentRoomInfo) before rethrowing a mapped application error.

Kody rule violation: Add try-catch blocks for external calls

Prompt for LLM

File src/stores/app/livekit-store.ts:

Line 412:

Unhandled rejection occurs in `src/stores/app/livekit-store.ts:413` and `417` because the `currentRoom.disconnect()` external network call is wrapped only in `try/finally`, violating Rule 27 by propagating teardown failures uncaught and unlogged. Add a `catch` block to log structured context (e.g., room SID, `currentRoomInfo`) before rethrowing a mapped application error.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants