Skip to content

Develop - #25

Merged
ucswift merged 4 commits into
masterfrom
develop
Jul 28, 2026
Merged

Develop#25
ucswift merged 4 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Jul 27, 2026

Copy link
Copy Markdown
Member

This PR introduces several major enhancements to the Incident Command board, notifications system, and overall app resilience.

Command Board Enhancements

Resource Details Sheet — Replaces direct remove/release buttons on resource cards with a comprehensive details inspector showing unit/personnel roster info, live status, staffing, assigned roles, and last known GPS coordinates. From the sheet, the IC can remove a resource from its lane, release it from the incident entirely, or open a fullscreen map of the resource's position. A new /resource-map screen renders a single resource marker on a fullscreen map.

Resource List Filtering — Added All / Unassigned / Assigned filter chips above the resources list to quickly narrow the view.

Lane Delete with Resource Disposition — Deleting a lane now prompts for what to do with its assigned resources: move them back to the unassigned pool or release them from the incident. The delete action is accessed from within the lane details editor.

Maps Tabbed Card — Consolidated the incident map and named tactical maps into a single tabbed pane, reducing vertical scroll space on the command board.

Message Commander — New feature allowing users to send free-form messages directly to the incident commander (and optionally deputy commanders) via their notification channels. Accessible from both the command board and call detail screen.

Fullscreen Command Log — The timeline section now links to a dedicated /command-log/[callId] screen with full-text search over all server-logged incident entries, replacing the inline "show more" pagination.

Real-Time Updates

Added SignalR handling for incidentCommandUpdated events, automatically refreshing the affected command board when the server reports changes (e.g., from another user's action).

Notifications

  • Extracted and shared the NotificationPayload type; Novu inbox items are now properly mapped to the app's payload shape with reference-type inference from event codes
  • Implemented navigation from notification references to call detail screens
  • Internationalized all notification UI strings
  • Changed the Novu subscriber ID to {DepartmentCode}_IC_User_{userId} and added Source: 'IC' to push registration, keeping the IC app's notification inbox separate from the Responder app

Resilience & Bug Fixes

  • Units store: Switched to Promise.allSettled so a failing status endpoint no longer breaks the unit roster; stale empty rosters are cleared from cache to force a server re-fetch
  • Call cards: Deduplicated dispatch entries by ID and name to prevent duplicate type badges
  • Unit roster retry: The resources "Add" button now triggers a fresh roster fetch if the list is empty

Other

  • Updated the release notes extraction script to replace standalone "PR" references with "Release"

Summary by CodeRabbit

  • New Features

    • Message incident commanders and optionally deputy commanders from calls and the Command Board.
    • View detailed resource information, locations, roles, and statuses; move resources between lanes, pools, or incidents.
    • Switch between incident and tactical maps, with improved map viewing.
    • Open a searchable, full-screen command log.
    • Filter resources by all, assigned, or unassigned status.
  • Bug Fixes

    • Improved notification deletion feedback and dispatch list consistency.
    • Improved loading and recovery when unit data is unavailable.
  • Localization

    • Added translations for new command, resource, log, and notification features.

@Resgrid-Bot

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ucswift, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 24 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 606520f8-7404-4906-9863-a3997005769b

📥 Commits

Reviewing files that changed from the base of the PR and between 52a7732 and 75c84bd.

📒 Files selected for processing (2)
  • src/app/(app)/command.tsx
  • src/stores/signalr/signalr-store.ts
📝 Walkthrough

Walkthrough

The update adds commander messaging, resource and lane detail workflows, tabbed maps, fullscreen command logs, notification mapping and localization, push-registration routing, realtime command synchronization, resilient unit loading, dispatch deduplication, and release-note normalization.

Changes

Command workflows

Layer / File(s) Summary
Commander messaging
src/models/v4/incidentCommand/*, src/api/incidentCommand/*, src/stores/command/store.ts, src/components/command/message-commander-sheet.tsx, src/app/(app)/command.tsx, src/app/call/[id].tsx
Adds typed commander messaging from the command board and call screen, with optional deputy delivery and success/failure handling.
Resource and lane management
src/components/command/resource-*.tsx, src/components/command/*structure*.tsx, src/components/command/lane-details-sheet.tsx, src/app/(app)/command.tsx
Replaces direct resource removal controls with detail sheets, adds lane deletion dispositions, resource filters, and lane editing.
Maps and command logs
src/components/command/maps-tabbed-card.tsx, src/components/command/*map*.tsx, src/components/command/timeline-section.tsx, src/app/command-log/*, src/app/resource-map/*
Adds tabbed incident/tactical maps, fullscreen searchable logs, and resource-position map navigation.
Command validation and translations
src/components/command/__tests__/*, src/app/(app)/__tests__/command.test.tsx, src/translations/*
Adds interaction coverage and localized strings for command messaging, resource details, lane deletion, logs, maps, and resource actions.

Notification inbox

Layer / File(s) Summary
Notification mapping and detail typing
src/components/notifications/*, src/types/notification.ts
Maps Novu notifications into typed payloads, infers call references from event codes, translates detail labels, and changes metadata values from any to unknown.
Notification actions and localization
src/components/notifications/NotificationInbox.tsx, src/translations/*
Internationalizes inbox states and deletion flows, refreshes after bulk deletion, and navigates call references with Expo Router.

Platform and data support

Layer / File(s) Summary
Push registration routing
src/models/v4/device/*, src/services/push-notification.ts, src/services/__tests__/*, src/app/(app)/_layout.tsx
Adds IC source markers to device registration and changes the Novu subscriber identifier format.
Realtime and store resilience
src/stores/signalr/*, src/stores/units/*, src/stores/calls/*
Handles incident command update events, settles unit requests independently, invalidates empty rosters, and deduplicates dispatches.
Release notes and list identity
scripts/extract-release-notes.sh, src/components/calls/call-card.tsx
Normalizes standalone PR tokens in release notes and updates dispatch item keys.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant CommandBoard
  participant MessageCommanderSheet
  participant CommandStore
  participant IncidentCommandAPI
  Operator->>CommandBoard: Open commander message
  CommandBoard->>MessageCommanderSheet: Render message form
  Operator->>MessageCommanderSheet: Submit message and deputy option
  MessageCommanderSheet->>CommandBoard: Return validated message
  CommandBoard->>CommandStore: Send commander message
  CommandStore->>IncidentCommandAPI: POST SendMessageToCommand
  IncidentCommandAPI-->>CommandStore: Return delivery count
  CommandStore-->>CommandBoard: Return success status
  CommandBoard-->>Operator: Show success or error toast
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is too generic and does not convey the main change in the pull request. Replace it with a concise, specific title that names the primary update, such as 'Add commander messaging and command log'.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate 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

Choose a reason for hiding this comment

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

Actionable comments posted: 16

Caution

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

⚠️ Outside diff range comments (1)
src/components/notifications/NotificationInbox.tsx (1)

246-275: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reconcile and log partial deletion failures.

Promise.all rejects on the first failed request while other deletes may still succeed. The catch skips refresh, leaves stale selected IDs, and discards the failure details. Use Promise.allSettled, retain only failed IDs, refresh after settlement, and log redacted error/count context before showing the toast.

As per coding guidelines, “Handle asynchronous operations with try/catch, logging failures and providing user feedback through toast notifications where appropriate.”

🤖 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/notifications/NotificationInbox.tsx` around lines 246 - 275,
The confirmBulkDelete flow should use Promise.allSettled so every selected
deletion completes, retain only IDs whose deletions failed, refresh
notifications after settlement, and update selection state to those failed IDs.
Log redacted failure details with relevant count context before showing the
existing error toast, while preserving the success toast and exit-selection
behavior when all deletions succeed.

Source: Coding guidelines

🧹 Nitpick comments (11)
src/app/resource-map/index.tsx (2)

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

Replace hardcoded fallback marker colors with semantic tokens.

Keep lane-provided colors dynamic, but use semantic error/background tokens for the default marker and border. As per coding guidelines: “use semantic Tailwind color tokens instead of hardcoded hex values.”

Also applies to: 72-72

🤖 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/resource-map/index.tsx` at line 26, Update the color fallback in the
resource-map marker styling to use the established semantic error/background
token instead of the hardcoded `#E53E3E` value, while preserving validated
lane-provided colors. Apply the same semantic token replacement to the default
border styling at the referenced second location.

Source: Coding guidelines


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

Use the configured environment import.

Import Env from @env, not @/lib/env, to preserve the project’s environment-access contract. As per coding guidelines: “Import environment values through Env from @env.”

🤖 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/resource-map/index.tsx` at line 11, Update the Env import in the
resource map module to use the configured `@env` alias instead of '`@/lib/env`',
preserving the project’s standard environment-access contract.

Source: Coding guidelines

src/app/call/[id].tsx (1)

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

Render newly added Lucide icons directly.

Render the Lucide icons directly rather than through gluestack wrappers:

  • src/app/call/[id].tsx: render MessageSquareIcon directly inside the button.
  • src/components/command/structure-section.tsx: render Eye directly instead of through Icon.
  • src/components/command/incident-maps-section.tsx: render Plus directly inside the button.
🤖 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/call/`[id].tsx at line 550, Render the Lucide icons directly instead
of through gluestack wrappers: in src/app/call/[id].tsx lines 550-550, update
the button to render MessageSquareIcon directly; in
src/components/command/structure-section.tsx lines 164-165, render Eye directly
instead of using Icon; and in src/components/command/incident-maps-section.tsx
line 78, render Plus directly inside the button.

Source: Coding guidelines

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

815-827: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Filter row can leave an empty list with no explanation.

With resourceFilter set to assigned/unassigned and no matches, the section renders nothing while the header count still shows the unfiltered total. A short "no matching resources" line would make the state self-explanatory.

🤖 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)/command.tsx around lines 815 - 827, Update the resource list
rendering near the resource-filter row and its section count so that when
resourceFilter is assigned or unassigned and the filtered results are empty, it
renders a short translated “no matching resources” message instead of only an
empty list. Keep the existing unfiltered rendering and header behavior unchanged
for filters with matches or the all option.
src/components/command/maps-tabbed-card.tsx (2)

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

Use a type-only import statement.

All three specifiers are types.

♻️ Proposed change
-import { type IncidentCommand, type IncidentMap, type IncidentMapAnnotation } from '`@/models/v4/incidentCommand/incidentCommandModels`';
+import type { IncidentCommand, IncidentMap, IncidentMapAnnotation } from '`@/models/v4/incidentCommand/incidentCommandModels`';

As per coding guidelines, "use import type for type-only imports".

🤖 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/command/maps-tabbed-card.tsx` at line 9, Update the import of
IncidentCommand, IncidentMap, and IncidentMapAnnotation to use a type-only
import declaration, preserving the existing specifiers and module source.

Source: Coding guidelines


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

Expose the tab selection state to assistive tech.

Visual solid/outline variance is the only selection cue; screen readers can't tell which pane is active.

♿ Proposed change
-        <Button size="xs" variant={tab === 'incident' ? 'solid' : 'outline'} onPress={() => setTab('incident')} testID="maps-tab-incident">
+        <Button size="xs" variant={tab === 'incident' ? 'solid' : 'outline'} accessibilityRole="tab" accessibilityState={{ selected: tab === 'incident' }} onPress={() => setTab('incident')} testID="maps-tab-incident">
           <ButtonText>{t('command.incident_map_section')}</ButtonText>
         </Button>
-        <Button size="xs" variant={tab === 'tactical' ? 'solid' : 'outline'} onPress={() => setTab('tactical')} testID="maps-tab-tactical">
+        <Button size="xs" variant={tab === 'tactical' ? 'solid' : 'outline'} accessibilityRole="tab" accessibilityState={{ selected: tab === 'tactical' }} onPress={() => setTab('tactical')} testID="maps-tab-tactical">

As per coding guidelines, "Follow WCAG mobile accessibility practices, including semantic components, accessible labels".

🤖 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/command/maps-tabbed-card.tsx` around lines 33 - 40, Update the
incident and tactical tab Buttons in the tab selector to expose their selected
state through the accessibility API, while preserving the existing visual
variants and tab switching behavior. Use the framework’s supported accessibility
property for selected tabs on both buttons, with each value derived from the
current tab state.

Source: Coding guidelines

src/app/command-log/[callId].tsx (3)

68-68: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Guard refresh against an empty callId.

The mount effect checks callId but the refresh handler doesn't, so a malformed route param fires a request with an empty id.

🤖 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/command-log/`[callId].tsx at line 68, Update the refresh handler on
the Button to validate callId before invoking fetchTimeline, matching the mount
effect’s existing guard; skip the request when callId is empty while preserving
refresh behavior for valid IDs.

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

Use @shopify/flash-list and hoist the render callbacks.

The log is unbounded, and renderItem, ListEmptyComponent, and contentContainerStyle are all re-created each render.

♻️ Proposed change
-import { FlatList } from 'react-native';
+import { FlashList } from '`@shopify/flash-list`';
+import { StyleSheet } from 'react-native';
@@
-      <FlatList
-        contentContainerStyle={{ padding: 12 }}
+      <FlashList
+        contentContainerStyle={styles.listContent}
         data={filtered}
-        initialNumToRender={30}
+        estimatedItemSize={48}
         keyExtractor={(entry) => entry.CommandLogEntryId}
-        ListEmptyComponent={
-          <Text className="py-8 text-center text-sm text-gray-500 dark:text-gray-400" testID="command-log-empty">
-            {search.trim() ? t('command.log_no_results') : t('command.empty_timeline')}
-          </Text>
-        }
-        renderItem={({ item: entry }) => (
-          <HStack className="mb-1.5 items-start rounded-lg bg-white px-3 py-2 dark:bg-gray-900" space="sm" testID={`command-log-entry-${entry.CommandLogEntryId}`}>
-            <Text className="shrink-0 text-xs tabular-nums text-gray-500 dark:text-gray-400">{formatTimestamp(entry.OccurredOn)}</Text>
-            <Text className="min-w-0 flex-1 text-sm text-gray-900 dark:text-white">{entry.Description}</Text>
-          </HStack>
-        )}
+        ListEmptyComponent={emptyComponent}
+        renderItem={renderLogEntry}
       />

Define renderLogEntry as a module-level (or useCallback) function and styles.listContent via StyleSheet.create, with emptyComponent memoized on the search/empty state.

As per coding guidelines, "Use @shopify/flash-list for performant lists ... avoid anonymous functions in renderItem and event handlers".

🤖 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/command-log/`[callId].tsx around lines 82 - 98, Replace the FlatList
in the command-log screen with `@shopify/flash-list`, and hoist the renderLogEntry
callback to module scope or memoize it with useCallback instead of defining it
inline. Move contentContainerStyle into a StyleSheet.create styles.listContent
definition, and memoize the emptyComponent based on the search/empty state while
preserving the existing translated messages and test ID.

Source: Coding guidelines


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

Icon-only buttons need accessible labels.

Neither the search toggle nor the refresh button exposes any text, so assistive tech announces nothing actionable.

♿ Proposed change
           <Button
             size="xs"
             variant={isSearchOpen ? 'solid' : 'outline'}
+            accessibilityRole="button"
+            accessibilityLabel={isSearchOpen ? t('common.close') : t('command.log_search_placeholder')}
             onPress={() => {
@@
-          <Button size="xs" variant="outline" onPress={() => fetchTimeline(callId)} testID="command-log-refresh">
+          <Button size="xs" variant="outline" accessibilityRole="button" accessibilityLabel={t('common.refresh')} onPress={() => fetchTimeline(callId)} testID="command-log-refresh">

As per coding guidelines, "Follow WCAG accessibility practices, including semantic components, accessible labels". Confirm the label keys exist in all locales.

🤖 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/command-log/`[callId].tsx around lines 57 - 70, Add accessible labels
to both icon-only buttons in the command-log view: give the search toggle a
label that reflects its current action/state and give the refresh button a
refresh label. Use the existing localization mechanism and confirm the
referenced label keys are defined in every locale.

Source: Coding guidelines

src/components/command/resource-details-sheet.tsx (2)

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

Use a semantic Tailwind token for the badge fallback instead of a hardcoded hex.

#6b7280 bypasses the theme and won't adapt to light/dark. Since the color is static, apply it via className and drop the style entry.

♻️ Proposed change
-    <Badge style={hex ? { backgroundColor: hex } : styles.badgeFallback} variant="solid" testID={testID}>
+    <Badge style={hex ? { backgroundColor: hex } : undefined} className={hex ? undefined : 'bg-gray-500 dark:bg-gray-600'} variant="solid" testID={testID}>
 const styles = StyleSheet.create({
-  badgeFallback: {
-    backgroundColor: '`#6b7280`',
-  },
   laneDot: {

As per coding guidelines, "Support light and dark modes, use semantic Tailwind color tokens instead of hardcoded hex values".

🤖 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/command/resource-details-sheet.tsx` at line 314, Replace the
hardcoded backgroundColor fallback with a semantic Tailwind color class applied
through className for the badge, and remove the corresponding style entry.
Preserve the existing fallback appearance while ensuring it adapts to light and
dark themes.

Source: Coding guidelines


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

Render lucide icons directly rather than through the gluestack Icon wrapper.

resource-cards.tsx in this same PR already renders <Eye className=... size={16} /> directly; keeping both patterns is inconsistent.

As per coding guidelines, "Use lucide-react-native icons directly, not through the gluestack Icon wrapper".

Also applies to: 239-239, 247-247

🤖 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/command/resource-details-sheet.tsx` at line 176, Update the
icon rendering in the resource details sheet, including the instances at the
highlighted locations, to render the lucide components directly instead of
wrapping them with gluestack’s Icon component. Preserve each icon’s existing
component selection, className, and size props, and remove the unnecessary Icon
wrapper and import.

Source: Coding guidelines

🤖 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 `@scripts/extract-release-notes.sh`:
- Around line 38-40: Update the sed expression in the cleaned_body
transformation to include underscore as a non-boundary character alongside
alphanumerics, preventing replacements within tokens such as PR_STATUS while
preserving standalone PR replacement. Add regression coverage for PR, PRs,
PROXY, and PR_STATUS.

In `@src/app/`(app)/command.tsx:
- Around line 478-495: The handleDeleteLane flow should not call deleteNode when
any moveResourceAssignment or releaseResourceAssignment operation reports a
blocked or failed outcome. Track each disposition result, surface a toast using
the existing notification mechanism when a step fails, and return before
deleting the lane; retain deletion only when all assignments are successfully
processed.

In `@src/app/command-log/`[callId].tsx:
- Line 27: Update the command-log selector around useCommandStore to use a
module-level typed empty-array constant instead of allocating [] in the ??
fallback; import CommandLogEntry from its existing declaration and preserve the
timeline selection behavior.

In `@src/app/resource-map/index.tsx`:
- Line 28: Update the isValid coordinate check in the resource map component to
require finite latitude within [-90, 90] and longitude within [-180, 180], while
preserving the existing non-zero coordinate requirement. Use this validation to
prevent invalid coordinates from reaching Mapbox.Camera or Mapbox.MarkerView.

In `@src/components/calls/call-card.tsx`:
- Line 167: Update the HStack key in the call-card mapping to use the stable
d.Id when available, falling back to d.Name or index only when no ID exists; do
not include index in keys for items with stable IDs.

In `@src/components/command/lane-details-sheet.tsx`:
- Line 152: Update the locale translation resources for
command.delete_lane_resources_message to add the required plural-form keys,
preserving the existing sentence as the base: en needs _one and _other; ar, pl,
and uk need _one, _few, _many, and _other; add matching plural forms for every
other locale using its existing sentence. Keep the resourceCount interpolation
intact so i18next selects the locale-appropriate form.

In `@src/components/command/message-commander-sheet.tsx`:
- Around line 45-56: Update handleSend to wrap the awaited onSend call in
try/catch/finally, resetting isSending in finally so rejection cannot leave the
sheet stuck; preserve the existing success path that closes the sheet when
onSend returns true.

In `@src/components/command/resource-details-sheet.tsx`:
- Around line 150-169: Update openResourceMap to store the 300 ms setTimeout
handle in a ref, and clear that pending timeout during component unmount cleanup
before it can call router.push. Preserve the existing delayed navigation and
parameter construction, and ensure the ref is reset after the callback runs.

In `@src/components/command/timeline-section.tsx`:
- Line 33: Update TimelineSection and TimelineSectionProps so callers rendering
the full log route must provide a callId, or add an equivalent fallback
expansion path when callId is absent. Ensure entries beyond VISIBLE_BATCH remain
accessible instead of limiting no-callId timelines to the initial batch.

In `@src/components/notifications/NotificationInbox.tsx`:
- Around line 38-47: Restrict referenceType to supported actionable values
before constructing the notification payload or triggering navigation, rather
than asserting arbitrary strings into the union. Update the reference resolution
around referenceType, referenceId, and eventCode to implement the documented N/M
event-code mappings, or expose only valid call references. Ensure unsupported
message, status, note, other, and unknown types do not dismiss the inbox without
a destination.

In `@src/stores/command/store.ts`:
- Around line 1651-1658: Update sendMessageToCommander to validate callId as a
positive safe integer without accepting trailing characters such as “42junk”;
use strict numeric parsing and require the resulting ID to be greater than zero
and within Number.MAX_SAFE_INTEGER. Capture the result of sendMessageToCommand
and return true only when result.Data is greater than zero; otherwise return
false.

In `@src/stores/signalr/signalr-store.ts`:
- Line 9: Update the useCommandStore import in signalr-store.ts to use the
configured "`@/stores/command/store`" path alias instead of the relative path,
without changing its usage.
- Around line 182-196: Add tests for the new store logic at
src/stores/signalr/signalr-store.ts:182-196 covering tracked-call refresh,
missing or untracked calls triggering full sync, and refresh failure handling;
at src/stores/units/store.ts:31-54 covering each Promise.allSettled failure path
and preserving cached roster data when fetches reject; and at
src/stores/calls/store.ts:144-152 verifying duplicate dispatches are removed
while distinct dispatches remain.
- Around line 182-195: Update the incidentCommandUpdated handler and its
command-board refresh flow to coalesce refreshes per callId: serialize
refreshBoard(callId) executions, track when an event arrives while a refresh is
in flight, and perform exactly one follow-up refresh for that callId after
completion when dirty. Preserve the existing warning logging and full sync
behavior for unknown or untracked incidents.

In `@src/stores/units/store.ts`:
- Around line 48-52: Update the empty-roster eviction logic in getUnits so
cacheManager.remove('/Units/GetAllUnits') runs only when the fetch fulfills with
an empty roster, not when a rejected request is represented as units = [].
Preserve the existing cache behavior for non-empty responses and failed fetches.

In `@src/translations/ar.json`:
- Around line 1025-1043: Update the Arabic notification count translations used
by confirm_delete_message and delete_success to include the configured v3 plural
suffixes: zero, one, two, few, many, and other, with Arabic text appropriate to
each count range. Keep the existing notification keys and interpolation intact,
and ensure both count-dependent message groups use the complete set.

---

Outside diff comments:
In `@src/components/notifications/NotificationInbox.tsx`:
- Around line 246-275: The confirmBulkDelete flow should use Promise.allSettled
so every selected deletion completes, retain only IDs whose deletions failed,
refresh notifications after settlement, and update selection state to those
failed IDs. Log redacted failure details with relevant count context before
showing the existing error toast, while preserving the success toast and
exit-selection behavior when all deletions succeed.

---

Nitpick comments:
In `@src/app/`(app)/command.tsx:
- Around line 815-827: Update the resource list rendering near the
resource-filter row and its section count so that when resourceFilter is
assigned or unassigned and the filtered results are empty, it renders a short
translated “no matching resources” message instead of only an empty list. Keep
the existing unfiltered rendering and header behavior unchanged for filters with
matches or the all option.

In `@src/app/call/`[id].tsx:
- Line 550: Render the Lucide icons directly instead of through gluestack
wrappers: in src/app/call/[id].tsx lines 550-550, update the button to render
MessageSquareIcon directly; in src/components/command/structure-section.tsx
lines 164-165, render Eye directly instead of using Icon; and in
src/components/command/incident-maps-section.tsx line 78, render Plus directly
inside the button.

In `@src/app/command-log/`[callId].tsx:
- Line 68: Update the refresh handler on the Button to validate callId before
invoking fetchTimeline, matching the mount effect’s existing guard; skip the
request when callId is empty while preserving refresh behavior for valid IDs.
- Around line 82-98: Replace the FlatList in the command-log screen with
`@shopify/flash-list`, and hoist the renderLogEntry callback to module scope or
memoize it with useCallback instead of defining it inline. Move
contentContainerStyle into a StyleSheet.create styles.listContent definition,
and memoize the emptyComponent based on the search/empty state while preserving
the existing translated messages and test ID.
- Around line 57-70: Add accessible labels to both icon-only buttons in the
command-log view: give the search toggle a label that reflects its current
action/state and give the refresh button a refresh label. Use the existing
localization mechanism and confirm the referenced label keys are defined in
every locale.

In `@src/app/resource-map/index.tsx`:
- Line 26: Update the color fallback in the resource-map marker styling to use
the established semantic error/background token instead of the hardcoded `#E53E3E`
value, while preserving validated lane-provided colors. Apply the same semantic
token replacement to the default border styling at the referenced second
location.
- Line 11: Update the Env import in the resource map module to use the
configured `@env` alias instead of '`@/lib/env`', preserving the project’s standard
environment-access contract.

In `@src/components/command/maps-tabbed-card.tsx`:
- Line 9: Update the import of IncidentCommand, IncidentMap, and
IncidentMapAnnotation to use a type-only import declaration, preserving the
existing specifiers and module source.
- Around line 33-40: Update the incident and tactical tab Buttons in the tab
selector to expose their selected state through the accessibility API, while
preserving the existing visual variants and tab switching behavior. Use the
framework’s supported accessibility property for selected tabs on both buttons,
with each value derived from the current tab state.

In `@src/components/command/resource-details-sheet.tsx`:
- Line 314: Replace the hardcoded backgroundColor fallback with a semantic
Tailwind color class applied through className for the badge, and remove the
corresponding style entry. Preserve the existing fallback appearance while
ensuring it adapts to light and dark themes.
- Line 176: Update the icon rendering in the resource details sheet, including
the instances at the highlighted locations, to render the lucide components
directly instead of wrapping them with gluestack’s Icon component. Preserve each
icon’s existing component selection, className, and size props, and remove the
unnecessary Icon wrapper and import.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b10738e8-44a4-4bc3-95ff-352f17de72ed

📥 Commits

Reviewing files that changed from the base of the PR and between c2590d7 and 70f61c5.

📒 Files selected for processing (46)
  • scripts/extract-release-notes.sh
  • src/api/incidentCommand/incidentCommand.ts
  • src/app/(app)/__tests__/command.test.tsx
  • src/app/(app)/_layout.tsx
  • src/app/(app)/command.tsx
  • src/app/call/[id].tsx
  • src/app/call/__tests__/[id].security.test.tsx
  • src/app/call/__tests__/[id].test.tsx
  • src/app/command-log/[callId].tsx
  • src/app/resource-map/index.tsx
  • src/components/calls/call-card.tsx
  • src/components/command/__tests__/lane-details-sheet.test.tsx
  • src/components/command/__tests__/message-commander-sheet.test.tsx
  • src/components/command/__tests__/resource-cards.test.tsx
  • src/components/command/__tests__/resource-details-sheet.test.tsx
  • src/components/command/__tests__/timeline-section.test.tsx
  • src/components/command/incident-map-card.tsx
  • src/components/command/incident-maps-section.tsx
  • src/components/command/landscape-structure-board.tsx
  • src/components/command/lane-details-sheet.tsx
  • src/components/command/maps-tabbed-card.tsx
  • src/components/command/message-commander-sheet.tsx
  • src/components/command/resource-cards.tsx
  • src/components/command/resource-details-sheet.tsx
  • src/components/command/structure-section.tsx
  • src/components/command/timeline-section.tsx
  • src/components/notifications/NotificationDetail.tsx
  • src/components/notifications/NotificationInbox.tsx
  • src/models/v4/device/pushRegistrationInput.ts
  • src/models/v4/incidentCommand/incidentCommandModels.ts
  • src/services/__tests__/push-notification.test.ts
  • src/services/push-notification.ts
  • src/stores/calls/store.ts
  • src/stores/command/store.ts
  • src/stores/signalr/signalr-store.ts
  • src/stores/units/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
  • src/types/notification.ts

Comment on lines +38 to +40
# Replace standalone occurrences of "PR" with "Release" (word boundaries only,
# so words like "PROXY" or "PRs" inside other tokens are untouched)
cleaned_body="$(printf '%s\n' "$cleaned_body" | sed -E 's/(^|[^[:alnum:]])PR([^[:alnum:]]|$)/\1Release\2/g')"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file excerpt =="
sed -n '1,90p' scripts/extract-release-notes.sh | cat -n

echo
echo "== sed boundary behavior probe =="
python3 - <<'PY'
import subprocess

pattern = r'<(^|[^\[:alnum:]])PR([^\[:alnum:]]|$)>\1Release\2/g'
# Try sed with equivalent raw POSIX pattern that matches alphanumeric boundary.
inputs = ["PR", "PR_STATUS", "xPRy", "PRx", "xPR", "PROXY", "PRs", "Release PR STATUS", "PR x"]
for p in [
    "s/(^|[^[:alnum:]])PR([^[:alnum:]]|$)/\\1Release\\2/g",
    "s/(^|[^[:alnum:]_])PR([^[:alnum:]_]|$)/\\1Release\\2/g",
]:
    print(f"-- pattern: {p}")
    for s in inputs:
        out = subprocess.check_output(["sed", "-E", p], text=True, input=f"{s}\n").rstrip()
        print(f"{s!r} -> {out!r}")
PY

Repository: Resgrid/IC

Length of output: 4668


Treat underscores as part of the token boundary.

[:alnum:] does not include _, so inputs like PR_STATUS or Release PR STATUS are incorrectly rewritten to Release_STATUS / Release Release STATUS. Include the underscore in both surrounding boundary classes.

Suggested fix
-  cleaned_body="$(printf '%s\n' "$cleaned_body" | sed -E 's/(^|[^[:alnum:]])PR([^[:alnum:]]|$)/\1Release\2/g')"
+  cleaned_body="$(printf '%s\n' "$cleaned_body" | sed -E 's/(^|[^[:alnum:]_])PR([^[:alnum:]_]|$)/\1Release\2/g')"

Add regression coverage for PR, PRs, PROXY, and PR_STATUS.

📝 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
# Replace standalone occurrences of "PR" with "Release" (word boundaries only,
# so words like "PROXY" or "PRs" inside other tokens are untouched)
cleaned_body="$(printf '%s\n' "$cleaned_body" | sed -E 's/(^|[^[:alnum:]])PR([^[:alnum:]]|$)/\1Release\2/g')"
# Replace standalone occurrences of "PR" with "Release" (word boundaries only,
# so words like "PROXY" or "PRs" inside other tokens are untouched)
cleaned_body="$(printf '%s\n' "$cleaned_body" | sed -E 's/(^|[^[:alnum:]_])PR([^[:alnum:]_]|$)/\1Release\2/g')"
🤖 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 `@scripts/extract-release-notes.sh` around lines 38 - 40, Update the sed
expression in the cleaned_body transformation to include underscore as a
non-boundary character alongside alphanumerics, preventing replacements within
tokens such as PR_STATUS while preserving standalone PR replacement. Add
regression coverage for PR, PRs, PROXY, and PR_STATUS.

Comment thread src/app/(app)/command.tsx
Comment thread src/app/command-log/[callId].tsx Outdated
Comment thread src/app/resource-map/index.tsx Outdated

return (
<HStack key={`${d.Id || index}-${d.Name}`} style={{ backgroundColor: 'rgba(255,255,255,0.25)', borderRadius: 6, overflow: 'hidden' }} className="items-center">
<HStack key={`${d.Id || 'noid'}-${d.Name}-${index}`} style={{ backgroundColor: 'rgba(255,255,255,0.25)', borderRadius: 6, overflow: 'hidden' }} className="items-center">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg '(^|/)src/components/calls/call-card\.tsx$|(^|/)src/app/\(app\)/calls\.tsx$' || true

echo "== outline call-card =="
if [ -f src/components/calls/call-card.tsx ]; then
  wc -l src/components/calls/call-card.tsx
  ast-grep outline src/components/calls/call-card.tsx --view compact || true
fi

echo "== relevant call-card section =="
if [ -f src/components/calls/call-card.tsx ]; then
  sed -n '120,190p' src/components/calls/call-card.tsx | cat -n
fi

echo "== search relevant keys/list data =="
rg -n "key=|dispatch|Id|Name|index" src/components/calls/call-card.tsx src/app/\(app\)/calls.tsx 2>/dev/null || true

echo "== deterministic key behavior probe =="
python3 - <<'PY'
def current_key(d, index):
    return f"{d.get('Id') or 'noid'}-{d.get('Name')}-{index}"
def suggested_key(d, index):
    return f"{d['Id']}-{d['Name']}" if d.get('Id') is not None else f"noid-{d['Name']}-{index}"

original = [{'Id': 'd1', 'Name': 'A'}, {'Id': 'd2', 'Name': 'B'}]
reordered = [{'Id': 'd2', 'Name': 'B'}, {'Id': 'd1', 'Name': 'A'}]
for label, data in [('original order', original), ('insert reordered after first', [{*original[0]}, original[1]]), ('reorder', reordered)]:
    print(label)
    prev = {}
    for i, d in enumerate(data):
        cur = suggested_key(d, i)
        print("  ", i, d, "current", current_key(d, i), "old_key", prev.get((d.get('Id'), d.get('Name'))), "current_changes_with_index?", current_key(d, i) != cur)
        prev[(d.get('Id'), d.get('Name'))] = current_key(d, i)
PY

Repository: Resgrid/IC

Length of output: 11138


Keep stable IDs out of index-based keys.

The key at src/components/calls/call-card.tsx:167 changes if an item moves, even when d.Id and d.Name stay the same, causing unnecessary row remounts. Use index only when the ID is unavailable.

Suggested fix
-                <HStack key={`${d.Id || 'noid'}-${d.Name}-${index}`} style={{ backgroundColor: 'rgba(255,255,255,0.25)', borderRadius: 6, overflow: 'hidden' }} className="items-center">
+                <HStack key={d.Id != null ? `${d.Id}-${d.Name}` : `noid-${d.Name}-${index}`} style={{ backgroundColor: 'rgba(255,255,255,0.25)', borderRadius: 6, overflow: 'hidden' }} className="items-center">
📝 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
<HStack key={`${d.Id || 'noid'}-${d.Name}-${index}`} style={{ backgroundColor: 'rgba(255,255,255,0.25)', borderRadius: 6, overflow: 'hidden' }} className="items-center">
<HStack key={d.Id != null ? `${d.Id}-${d.Name}` : `noid-${d.Name}-${index}`} style={{ backgroundColor: 'rgba(255,255,255,0.25)', borderRadius: 6, overflow: 'hidden' }} className="items-center">
🤖 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/calls/call-card.tsx` at line 167, Update the HStack key in the
call-card mapping to use the stable d.Id when available, falling back to d.Name
or index only when no ID exists; do not include index in keys for items with
stable IDs.

import { signalRService } from '@/services/signalr.service';

import { useCoreStore } from '../app/core-store';
import { useCommandStore } from '../command/store';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Use the configured path alias.

Replace the relative store import with @/stores/command/store. As per coding guidelines, “Use configured path aliases (@/*, @env, @assets/*) instead of relative imports.”

🤖 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/signalr/signalr-store.ts` at line 9, Update the useCommandStore
import in signalr-store.ts to use the configured "`@/stores/command/store`" path
alias instead of the relative path, without changing its usage.

Source: Coding guidelines

Comment thread src/stores/signalr/signalr-store.ts
Comment on lines +182 to +196
signalRService.on('incidentCommandUpdated', (message) => {
set({ lastUpdateMessage: JSON.stringify(message), lastUpdateTimestamp: Date.now() });
const callId = extractCommandCallId(message);
const commandState = useCommandStore.getState();
if (callId && commandState.boards[callId]) {
commandState.refreshBoard(callId).catch((error) => {
logger.warn({ message: 'incidentCommandUpdated: failed to refresh board', context: { callId, error } });
});
} else {
// Unknown or untracked incident — resync the full bundle.
commandState.syncFromServer().catch((error) => {
logger.warn({ message: 'incidentCommandUpdated: failed to sync from server', context: { message, error } });
});
}
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Add coverage for the new store behavior.

  • src/stores/signalr/signalr-store.ts#L182-L196: test tracked-call refresh, missing/untracked-call full sync, and refresh failure handling.
  • src/stores/units/store.ts#L31-L54: test each Promise.allSettled failure path and ensure rejected roster fetches do not evict cached data.
  • src/stores/calls/store.ts#L144-L152: test duplicate dispatch removal while preserving distinct dispatches.

As per coding guidelines, “generate tests for new components, services, and logic.”

📍 Affects 3 files
  • src/stores/signalr/signalr-store.ts#L182-L196 (this comment)
  • src/stores/units/store.ts#L31-L54
  • src/stores/calls/store.ts#L144-L152
🤖 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/signalr/signalr-store.ts` around lines 182 - 196, Add tests for
the new store logic at src/stores/signalr/signalr-store.ts:182-196 covering
tracked-call refresh, missing or untracked calls triggering full sync, and
refresh failure handling; at src/stores/units/store.ts:31-54 covering each
Promise.allSettled failure path and preserving cached roster data when fetches
reject; and at src/stores/calls/store.ts:144-152 verifying duplicate dispatches
are removed while distinct dispatches remain.

Source: Coding guidelines

Comment thread src/stores/units/store.ts
Comment thread src/translations/ar.json
Comment on lines +1025 to +1043
"notifications": {
"additional_info": "معلومات إضافية",
"confirm_delete_message_one": "هل أنت متأكد من حذف {{count}} إشعار؟ لا يمكن التراجع عن هذا الإجراء.",
"confirm_delete_message_other": "هل أنت متأكد من حذف {{count}} إشعارات؟ لا يمكن التراجع عن هذا الإجراء.",
"confirm_delete_title": "تأكيد الحذف",
"delete_error": "فشل حذف الإشعارات",
"delete_one_error": "فشل حذف الإشعار",
"delete_one_success": "تم حذف الإشعار",
"delete_success_one": "تم حذف {{count}} إشعار",
"delete_success_other": "تم حذف {{count}} إشعارات",
"deselect_all": "إلغاء تحديد الكل",
"empty": "لا توجد تحديثات متاحة",
"load_error": "تعذر تحميل الإشعارات",
"notification": "إشعار",
"select_all": "تحديد الكل",
"selected_count": "{{count}} محدد",
"title": "الإشعارات",
"view_reference": "عرض {{type}}"
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "compatibilityJSON|plural|i18next" package.json src

Repository: Resgrid/IC

Length of output: 18224


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== i18n config =="
sed -n '1,80p' src/lib/i18n/index.tsx

echo
echo "== notification translation keys across Arabic and English =="
python3 - <<'PY'
import json
from pathlib import Path
for name in ['en.json','ar.json']:
    path = Path('src/translations', name)
    data = json.loads(path.read_text())
    keys = sorted(data.get('notifications',{}).keys())
    print(name, '\n'.join(keys))
PY

echo
echo "== i18next plural behavior probe for v3 v4 if dependencies allow =="
python3 - <<'PY'
try:
    from i18next_plural_rules import plural
    for count in [0,1,2,3,4,5,6,7,8,9,10,11,12,20,42]:
        print(count, plural(count, 'ar', 'v4'), plural(count, 'ar', 'v3'))
except Exception as e:
    print("probe unavailable:", type(e).__name__, e)
PY

echo
echo "== translation strings mentioning one/zero/two/few/many =="
rg -n "notifications\\..*_one|notifications\\..*_zero|notification\\..*_one|notification\\..*_zero|notifications\\..*_two|notification\\�*\\._two|notifications\\.*_few|notification\\.*_few|notifications\\.*_many|notification\\.*_many" src/translations/ar.json src/translations/en.json

Repository: Resgrid/IC

Length of output: 2721


🌐 Web query:

i18next compatibilityJSON v3 plural rules Arabic one other counts 0 2 3-10

💡 Result:

The compatibilityJSON v3 option was a legacy feature used in i18next versions 21 through 23 to maintain backward compatibility with older JSON plural formats [1][2][3]. As of i18next version 24, this option has been removed entirely; i18next now mandates the use of the Intl.PluralRules API [1][2][4]. In the legacy v3 format, languages with multiple plural forms—such as Arabic—used specific numeric suffixes in the JSON keys to map to different plural categories [1][5][6]. Under that older system, Arabic resources typically utilized suffixes like _0, _1, _2, _3, _11, and _100 [7]. In contrast, modern i18next (v4+ format) uses suffixes aligned with the Intl.PluralRules categories (zero, one, two, few, many, other) [8][9][3]. For Arabic, these correspond to the following count mappings [8][9]: { "key_zero": "zero", "key_one": "singular", "key_two": "two", "key_few": "few", "key_many": "many", "key_other": "other" } If you are using a modern version of i18next (v24 or later) and encounter issues with pluralization, you must ensure the Intl.PluralRules API is available in your environment [2][4]. If it is not supported (for example, in certain React Native/Hermes environments), you must include an Intl polyfill (e.g., intl-pluralrules) to restore full plural support [8][2][4]. Without this, i18next will default to a minimal English-style rule supporting only _one and _other [2][9][4].

Citations:


Keep Arabic count keys in sync with the configured plural mode.

The app runs compatibilityJSON: 'v3', where Arabic still needs count-specific suffixes beyond one/other. Translate the Arabic count variants (0, 1, 2, 3-10, 11-100, 100+) for the notification delete strings, or switch the i18next plural mode and add Intl plural-rule support for Arabic.

🤖 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` around lines 1025 - 1043, Update the Arabic
notification count translations used by confirm_delete_message and
delete_success to include the configured v3 plural suffixes: zero, one, two,
few, many, and other, with Arabic text appropriate to each count range. Keep the
existing notification keys and interpolation intact, and ensure both
count-dependent message groups use the complete set.


/** Sends a free-form message directly to the incident's commander (and optionally deputies) via their notification channels. */
export const sendMessageToCommand = async (input: SendMessageToCommandInput) => {
const response = await createApiEndpoint('/IncidentCommand/SendMessageToCommand').post<SendMessageToCommandResult>({ ...input });

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

Missing error handling violates Rule [28] by exposing the network POST call without contextual wrapping. Enclose the external request in a try/catch block, log contextual data such as endpoint names and input identifiers, and map the result to an application-level domain error.

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

Prompt for LLM

File src/api/incidentCommand/incidentCommand.ts:

Line 68:

Missing error handling violates Rule [28] by exposing the network POST call without contextual wrapping. Enclose the external request in a try/catch block, log contextual data such as endpoint names and input identifiers, and map the result to an application-level domain error.

Talk to Kody by mentioning @kody

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

Comment on lines +66 to +67
/** Sends a free-form message directly to the incident's commander (and optionally deputies) via their notification channels. */
export const sendMessageToCommand = async (input: SendMessageToCommandInput) => {

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 return type documentation violates Rule [23] for async functions. Update the JSDoc to include @returns {Promise<SendMessageToCommandResult>} and document possible rejection scenarios.

Kody rule violation: Document async/Promise behavior and errors

Prompt for LLM

File src/api/incidentCommand/incidentCommand.ts:

Line 66 to 67:

Missing return type documentation violates Rule [23] for async functions. Update the JSDoc to include `@returns {Promise<SendMessageToCommandResult>}` and document possible rejection scenarios.

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)/command.tsx Outdated
<ButtonIcon as={UserCog} />
</Button>
<Button
onPress={() => setIsMessageSheetOpen(true)}

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

Render performance degradation caused by inline arrow functions and .bind() usage in JSX props across multiple components. Move these function definitions outside the render method to prevent unnecessary function recreation during every render cycle.

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

Prompt for LLM

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

Line 608:

Render performance degradation caused by inline arrow functions and `.bind()` usage in JSX props across multiple components. Move these function definitions outside the render method to prevent unnecessary function recreation during every render cycle.

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)/command.tsx Outdated
return;
}
const laneAssignments = (boards[activeBoardCallId]?.board?.Assignments ?? []).filter((a) => !a.ReleasedOn && a.CommandStructureNodeId === nodeId);
for (const assignment of laneAssignments) {

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

Network performance bottleneck identified as an N+1 pattern where the loop issues sequential awaited calls to moveResourceAssignment and releaseResourceAssignment. Batch these independent operations using Promise.all or Promise.allSettled before calling deleteNode to reduce round-trips.

Kody rule violation: Detect N+1 style queries and suggest batching

Prompt for LLM

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

Line 484:

Network performance bottleneck identified as an N+1 pattern where the loop issues sequential awaited calls to `moveResourceAssignment` and `releaseResourceAssignment`. Batch these independent operations using `Promise.all` or `Promise.allSettled` before calling `deleteNode` to reduce round-trips.

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)/command.tsx
maps={boardState.board?.Maps ?? []}
users={users}
onSave={(nodeId, patch) => updateNodeDetails(boardState.callId, nodeId, patch)}
resourceCount={(boardState.board?.Assignments ?? []).filter((a) => !a.ReleasedOn && a.CommandStructureNodeId === editLaneNodeId).length}

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

Logic duplication in the active-assignment predicate risks divergence from the identical logic at line 483 in handleDeleteLane. Extract a shared helper like countActiveLaneAssignments(assignments, nodeId) to centralize the filtering criteria for both the handler and the JSX prop.

Kody rule violation: Extract common query logic

Prompt for LLM

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

Line 920:

Logic duplication in the active-assignment predicate risks divergence from the identical logic at line 483 in `handleDeleteLane`. Extract a shared helper like `countActiveLaneAssignments(assignments, nodeId)` to centralize the filtering criteria for both the handler and the JSX prop.

Talk to Kody by mentioning @kody

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

Comment thread src/app/command-log/[callId].tsx Outdated
if (!query) {
return entries;
}
return entries.filter((entry) => entry.Description.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 high

Null pointer dereference crashes the screen when the filter callback invokes .toLowerCase() on a potentially undefined entry.Description. Apply optional chaining via entry.Description?.toLowerCase().includes(query) ?? false to guard against nullish values.

Kody rule violation: Add null checks before accessing properties

Prompt for LLM

File src/app/command-log/[callId].tsx:

Line 44:

Null pointer dereference crashes the screen when the filter callback invokes `.toLowerCase()` on a potentially undefined `entry.Description`. Apply optional chaining via `entry.Description?.toLowerCase().includes(query) ?? false` to guard against nullish values.

Talk to Kody by mentioning @kody

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

};

/** Fullscreen incident log — every server-logged entry with text search. */
export default function CommandLogScreen() {

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

Default export reduces refactor safety and IDE auto-import reliability compared to named exports. Change to a named export like export function CommandLogScreen() unless Expo Router strictly requires a default export for route screens.

Kody rule violation: Avoid default exports

Prompt for LLM

File src/app/command-log/[callId].tsx:

Line 22:

Default export reduces refactor safety and IDE auto-import reliability compared to named exports. Change to a named export like `export function CommandLogScreen()` unless Expo Router strictly requires a default export for route screens.

Talk to Kody by mentioning @kody

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


return (
<HStack key={`${d.Id || index}-${d.Name}`} style={{ backgroundColor: 'rgba(255,255,255,0.25)', borderRadius: 6, overflow: 'hidden' }} className="items-center">
<HStack key={`${d.Id || 'noid'}-${d.Name}-${index}`} style={{ backgroundColor: 'rgba(255,255,255,0.25)', borderRadius: 6, overflow: 'hidden' }} className="items-center">

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

React reconciliation bug triggered by embedding the array index alongside d.Id and d.Name in list keys. Derive a stable unique key from a genuine identifier without mixing in the positional array index.

Kody rule violation: Avoid array indexes as keys in React lists

Prompt for LLM

File src/components/calls/call-card.tsx:

Line 167:

React reconciliation bug triggered by embedding the array `index` alongside `d.Id` and `d.Name` in list keys. Derive a stable unique key from a genuine identifier without mixing in the positional array index.

Talk to Kody by mentioning @kody

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

Comment on lines +89 to +92
<Button size="xs" variant="outline" onPress={() => setIsAddOpen(true)} testID="incident-maps-add">
<ButtonIcon as={Plus} />
<ButtonText>{t('command.add')}</ButtonText>
</Button>

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

Code duplication violates Rule [15] by repeating the add-button JSX block in both the embedded and non-embedded branches. Extract the block into a named utility component like <IncidentMapAddButton onPress=... /> to prevent synchronization drift.

Kody rule violation: Extract duplicated logic into functions

Prompt for LLM

File src/components/command/incident-maps-section.tsx:

Line 89 to 92:

Code duplication violates Rule [15] by repeating the add-button JSX block in both the `embedded` and non-embedded branches. Extract the block into a named utility component like `<IncidentMapAddButton onPress=... />` to prevent synchronization drift.

Talk to Kody by mentioning @kody

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

/** Active resources sitting in this lane — drives the delete confirmation options. */
resourceCount?: number;
/** Delete the lane; disposition decides what happens to its resources first. */
onDelete?: (commandStructureNodeId: string, disposition: 'pool' | 'release') => void;

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

Type drift risk identified as the 'pool' | 'release' union type duplicates string literals used at runtime across multiple lines. Declare a const tuple like const LANE_DISPOSITIONS = ['pool', 'release'] as const; and derive the type from it to ensure single-source-of-truth maintenance.

Kody rule violation: Derive TypeScript types from validation schemas

Prompt for LLM

File src/components/command/lane-details-sheet.tsx:

Line 46:

Type drift risk identified as the `'pool' | 'release'` union type duplicates string literals used at runtime across multiple lines. Declare a const tuple like `const LANE_DISPOSITIONS = ['pool', 'release'] as const;` and derive the type from it to ensure single-source-of-truth maintenance.

Talk to Kody by mentioning @kody

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

return (
<Box className="rounded-xl bg-white p-4 shadow-sm dark:bg-gray-800" testID="command-maps-pane">
<HStack space="sm" className="mb-3">
<Button size="xs" variant={tab === 'incident' ? 'solid' : 'outline'} onPress={() => setTab('incident')} testID="maps-tab-incident">

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

Maintenance risk identified from raw string literals like 'incident' replacing a strongly typed MapsTab union. Derive the type from a const tuple and reference named constants to eliminate magic strings.

Kody rule violation: Use enums instead of magic strings

Prompt for LLM

File src/components/command/maps-tabbed-card.tsx:

Line 34:

Maintenance risk identified from raw string literals like `'incident'` replacing a strongly typed `MapsTab` union. Derive the type from a const tuple and reference named constants to eliminate magic strings.

Talk to Kody by mentioning @kody

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

return;
}
setIsSending(true);
const ok = await onSend(title.trim() || null, trimmedBody, includeDeputies);

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 from the awaited onSend(...) call on line 51 leaves isSending at true and the UI in a perpetual loading state. Wrap the await in a try/catch block, reset isSending in a finally block, and log the error context.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File src/components/command/message-commander-sheet.tsx:

Line 51:

Unhandled promise rejection from the awaited `onSend(...)` call on line 51 leaves `isSending` at `true` and the UI in a perpetual loading state. Wrap the await in a try/catch block, reset `isSending` in a `finally` block, and log the error context.

Talk to Kody by mentioning @kody

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

}, [body, title, includeDeputies, isSending, onSend, handleClose]);

return (
<CustomBottomSheet isOpen={isOpen} onClose={handleClose} snapPoints={[75]}>

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 number obfuscation occurs in the snapPoints={[75]} prop, making maintenance error-prone. Extract the viewport height percentage to a named constant like const SHEET_SNAP_POINT_PERCENT = 75;.

Kody rule violation: Replace magic numbers with named constants

Prompt for LLM

File src/components/command/message-commander-sheet.tsx:

Line 59:

Magic number obfuscation occurs in the `snapPoints={[75]}` prop, making maintenance error-prone. Extract the viewport height percentage to a named constant like `const SHEET_SNAP_POINT_PERCENT = 75;`.

Talk to Kody by mentioning @kody

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

<Button size="xs" variant="outline" onPress={() => setVisibleCount((c) => c + VISIBLE_BATCH)} testID="command-timeline-more">
<ButtonText>{t('command.show_more')}</ButtonText>
{callId && entries.length > VISIBLE_BATCH ? (
<Button size="xs" variant="outline" onPress={() => router.push(`/command-log/${callId}` as never)} testID="command-timeline-view-log">

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

Type safety bypass occurs because the route path uses the unsafe as never assertion to bypass expo-router's typed-routes checking. Register the route correctly or use a more specific assertion like as \/command-log/${string}`` to preserve compile-time route validation.

Kody rule violation: Use safe type casting with as operator

Prompt for LLM

File src/components/command/timeline-section.tsx:

Line 58:

Type safety bypass occurs because the route path uses the unsafe `as never` assertion to bypass expo-router's typed-routes checking. Register the route correctly or use a more specific assertion like `as \`/command-log/${string}\`` to preserve compile-time route validation.

Talk to Kody by mentioning @kody

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

@@ -224,40 +251,41 @@ export const NotificationInbox = ({ isOpen, onClose }: NotificationInboxProps) =
const deletePromises = Array.from(selectedNotificationIds).map((id) => deleteMessage(id));
await Promise.all(deletePromises);

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

Partial-failure mishandling occurs because Promise.all short-circuits during a bulk-delete operation, silently abandoning remaining deletions. Use Promise.allSettled to process all deletions and report per-item successes and failures to the user.

Kody rule violation: Use Promise.allSettled for batch operations with partial failures

Prompt for LLM

File src/components/notifications/NotificationInbox.tsx:

Line 252:

Partial-failure mishandling occurs because `Promise.all` short-circuits during a bulk-delete operation, silently abandoning remaining deletions. Use `Promise.allSettled` to process all deletions and report per-item successes and failures to the user.

Talk to Kody by mentioning @kody

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

Platform: Platform.OS === 'ios' ? 1 : 2,
DeviceUuid: getDeviceUuid() || '',
Prefix: departmentCode,
Source: 'IC',

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

Maintenance fragility from the inline string literal 'IC' acting as a source-type identifier across multiple call sites. Define a centralized enum or constant object like export const NOTIFICATION_SOURCE = { IC: 'IC', Responder: 'Responder' } as const; and reference it to prevent duplication.

Kody rule violation: Centralize string constants

Prompt for LLM

File src/services/push-notification.ts:

Line 370:

Maintenance fragility from the inline string literal `'IC'` acting as a source-type identifier across multiple call sites. Define a centralized enum or constant object like `export const NOTIFICATION_SOURCE = { IC: 'IC', Responder: 'Responder' } as const;` and reference it to prevent duplication.

Talk to Kody by mentioning @kody

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

Comment on lines +182 to +196
signalRService.on('incidentCommandUpdated', (message) => {
set({ lastUpdateMessage: JSON.stringify(message), lastUpdateTimestamp: Date.now() });
const callId = extractCommandCallId(message);
const commandState = useCommandStore.getState();
if (callId && commandState.boards[callId]) {
commandState.refreshBoard(callId).catch((error) => {
logger.warn({ message: 'incidentCommandUpdated: failed to refresh board', context: { callId, error } });
});
} else {
// Unknown or untracked incident — resync the full bundle.
commandState.syncFromServer().catch((error) => {
logger.warn({ message: 'incidentCommandUpdated: failed to sync from server', context: { message, error } });
});
}
});

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

Unthrottled network traffic and redundant reprocessing occur because the incidentCommandUpdated handler invokes syncFromServer() for every department-wide event. Debounce the syncFromServer() call or limit its execution to when the user actively opens or lists incidents.

// Use a trailing debounce so a burst of department-wide incidentCommandUpdated
// events collapses into a single full sync instead of one per event.
let incidentCommandResyncTimer: ReturnType<typeof setTimeout> | null = null;
const debouncedFullSync = () => {
  if (incidentCommandResyncTimer) {
    clearTimeout(incidentCommandResyncTimer);
  }
  incidentCommandResyncTimer = setTimeout(() => {
    incidentCommandResyncTimer = null;
    useCommandStore.getState().syncFromServer().catch((error) => {
      logger.warn({ message: 'incidentCommandUpdated: failed to sync from server', context: { error } });
    });
  }, 2000);
};

signalRService.on('incidentCommandUpdated', (message) => {
  set({ lastUpdateMessage: JSON.stringify(message), lastUpdateTimestamp: Date.now() });
  const callId = extractCommandCallId(message);
  const commandState = useCommandStore.getState();
  if (callId && commandState.boards[callId]) {
    commandState.refreshBoard(callId).catch((error) => {
      logger.warn({ message: 'incidentCommandUpdated: failed to refresh board', context: { callId, error } });
    });
  } else {
    debouncedFullSync();
  }
});
Prompt for LLM

File src/stores/signalr/signalr-store.ts:

Line 182 to 196:

Unthrottled network traffic and redundant reprocessing occur because the `incidentCommandUpdated` handler invokes `syncFromServer()` for every department-wide event. Debounce the `syncFromServer()` call or limit its execution to when the user actively opens or lists incidents.

Suggested Code:

// Use a trailing debounce so a burst of department-wide incidentCommandUpdated
// events collapses into a single full sync instead of one per event.
let incidentCommandResyncTimer: ReturnType<typeof setTimeout> | null = null;
const debouncedFullSync = () => {
  if (incidentCommandResyncTimer) {
    clearTimeout(incidentCommandResyncTimer);
  }
  incidentCommandResyncTimer = setTimeout(() => {
    incidentCommandResyncTimer = null;
    useCommandStore.getState().syncFromServer().catch((error) => {
      logger.warn({ message: 'incidentCommandUpdated: failed to sync from server', context: { error } });
    });
  }, 2000);
};

signalRService.on('incidentCommandUpdated', (message) => {
  set({ lastUpdateMessage: JSON.stringify(message), lastUpdateTimestamp: Date.now() });
  const callId = extractCommandCallId(message);
  const commandState = useCommandStore.getState();
  if (callId && commandState.boards[callId]) {
    commandState.refreshBoard(callId).catch((error) => {
      logger.warn({ message: 'incidentCommandUpdated: failed to refresh board', context: { callId, error } });
    });
  } else {
    debouncedFullSync();
  }
});

Talk to Kody by mentioning @kody

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

Comment thread src/stores/signalr/signalr-store.ts Outdated
} else {
// Unknown or untracked incident — resync the full bundle.
commandState.syncFromServer().catch((error) => {
logger.warn({ message: 'incidentCommandUpdated: failed to sync from server', context: { message, error } });

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

GDPR data minimization violation identified by logging the raw SignalR message payload, which may contain unredacted PII, directly into the warn context. Log only the extracted callId or pass the full payload through a PII-redaction utility before logging.

Kody rule violation: Redact PII in logs and metrics by default

Prompt for LLM

File src/stores/signalr/signalr-store.ts:

Line 193:

GDPR data minimization violation identified by logging the raw SignalR `message` payload, which may contain unredacted PII, directly into the warn context. Log only the extracted `callId` or pass the full payload through a PII-redaction utility before logging.

Talk to Kody by mentioning @kody

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

@Resgrid-Bot

This comment has been minimized.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (2)
src/app/(app)/command.tsx (2)

812-818: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Retry the personnel roster as well as the unit roster.

AddResourceSheet receives personnel={users}, but this handler only retries fetchUnits(). If the initial user fetch failed, opening the resource picker still leaves personnel unavailable. Add a users.length === 0 retry for fetchUsers().

Proposed fix
               if (units.length === 0) {
                 fetchUnits();
               }
+              if (users.length === 0) {
+                fetchUsers();
+              }
               setIsResourceSheetOpen(true);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(app)/command.tsx around lines 812 - 818, Update the
AddResourceSheet onAdd handler in command.tsx to also call fetchUsers() when
users.length === 0, alongside the existing fetchUnits() retry, before opening
the resource sheet.

834-858: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve lane context when opening resource details.

Both resource-card handlers always set context: 'pool', even when CommandStructureNodeId is populated. An assigned resource therefore shows “Release from incident” and can be released entirely instead of being moved back to the pool.

Proposed fix
- onView={() => setViewResource({ assignment, context: 'pool' })}
+ onView={() => setViewResource({ assignment, context: assignment.CommandStructureNodeId ? 'lane' : 'pool' })}

Apply this to both the unit and personnel cards.

🤖 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)/command.tsx around lines 834 - 858, Update both resource-card
onView handlers in the filteredDeptAssignments map to derive context from
assignment.CommandStructureNodeId instead of always using 'pool'. Preserve
'pool' for unassigned resources and use the incident/lane context when the node
ID is populated, for both UnitResourceCard and PersonnelResourceCard.
♻️ Duplicate comments (1)
src/app/(app)/command.tsx (1)

495-498: ⚠️ Potential issue | 🟠 Major

Still abort lane deletion when a release disposition fails.

The pool branch checks outcomes, but the release branch discards Promise.all(...) results and proceeds to deleteNode. Apply the same failure check and user feedback before deleting the lane.

🤖 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)/command.tsx around lines 495 - 498, Update the release branch
in the lane-deletion flow around releaseResourceAssignment and deleteNode to
inspect the Promise.all results, matching the pool branch’s failure handling. If
any release disposition fails, provide the same user feedback and return before
calling deleteNode; only delete the lane when all assignments release
successfully.
🤖 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)/command.tsx:
- Around line 607-612: Add localized accessibilityLabel values using t() to the
icon-only buttons invoking handleOpenCommandDetails and handleOpenTransfer.
Match the existing localized labeling pattern used by the message and
end-command buttons, with labels that clearly identify editing details and
transferring the command.

In `@src/stores/signalr/signalr-store.ts`:
- Around line 79-91: Update debouncedFullSync and its surrounding state to
serialize syncFromServer calls: track whether a full sync is in flight and
whether another event arrived while it was pending, then run one follow-up sync
after completion when dirty. Preserve the existing debounce behavior and
logger.warn handling while ensuring concurrent requests cannot overwrite newer
state with an older response.

---

Outside diff comments:
In `@src/app/`(app)/command.tsx:
- Around line 812-818: Update the AddResourceSheet onAdd handler in command.tsx
to also call fetchUsers() when users.length === 0, alongside the existing
fetchUnits() retry, before opening the resource sheet.
- Around line 834-858: Update both resource-card onView handlers in the
filteredDeptAssignments map to derive context from
assignment.CommandStructureNodeId instead of always using 'pool'. Preserve
'pool' for unassigned resources and use the incident/lane context when the node
ID is populated, for both UnitResourceCard and PersonnelResourceCard.

---

Duplicate comments:
In `@src/app/`(app)/command.tsx:
- Around line 495-498: Update the release branch in the lane-deletion flow
around releaseResourceAssignment and deleteNode to inspect the Promise.all
results, matching the pool branch’s failure handling. If any release disposition
fails, provide the same user feedback and return before calling deleteNode; only
delete the lane when all assignments release successfully.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e32a968f-4f6e-49f7-9317-895acbed0dc2

📥 Commits

Reviewing files that changed from the base of the PR and between 70f61c5 and 52a7732.

📒 Files selected for processing (18)
  • src/app/(app)/command.tsx
  • src/app/command-log/[callId].tsx
  • src/app/resource-map/index.tsx
  • src/components/command/message-commander-sheet.tsx
  • src/components/command/resource-details-sheet.tsx
  • src/components/notifications/NotificationInbox.tsx
  • src/stores/command/store.ts
  • src/stores/signalr/signalr-store.ts
  • src/stores/units/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
🚧 Files skipped from review as they are similar to previous changes (16)
  • src/app/resource-map/index.tsx
  • src/translations/uk.json
  • src/translations/pl.json
  • src/components/command/resource-details-sheet.tsx
  • src/translations/sv.json
  • src/app/command-log/[callId].tsx
  • src/stores/units/store.ts
  • src/stores/command/store.ts
  • src/components/command/message-commander-sheet.tsx
  • src/translations/es.json
  • src/translations/en.json
  • src/translations/fr.json
  • src/translations/it.json
  • src/translations/ar.json
  • src/translations/de.json
  • src/components/notifications/NotificationInbox.tsx

Comment thread src/app/(app)/command.tsx Outdated
Comment thread src/stores/signalr/signalr-store.ts
Comment thread src/app/(app)/command.tsx
}
const laneAssignments = (boards[activeBoardCallId]?.board?.Assignments ?? []).filter((a) => !a.ReleasedOn && a.CommandStructureNodeId === nodeId);
if (disposition === 'pool') {
const outcomes = await Promise.all(laneAssignments.map((assignment) => moveResourceAssignment(activeBoardCallId, assignment.ResourceAssignmentId, '')));

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 risk: the awaited Promise.all at line 496 has no surrounding try/catch or .catch handler, so if any moveResourceAssignment rejects, the error propagates uncaught through the callback. Wrap the await in try/catch, log structured context (callId, nodeId, disposition), and surface a user-facing notification.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

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

Line 490:

Unhandled promise rejection risk: the awaited Promise.all at line 496 has no surrounding try/catch or .catch handler, so if any moveResourceAssignment rejects, the error propagates uncaught through the callback. Wrap the await in try/catch, log structured context (callId, nodeId, disposition), and surface a user-facing notification.

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)/command.tsx
}
const laneAssignments = (boards[activeBoardCallId]?.board?.Assignments ?? []).filter((a) => !a.ReleasedOn && a.CommandStructureNodeId === nodeId);
if (disposition === 'pool') {
const outcomes = await Promise.all(laneAssignments.map((assignment) => moveResourceAssignment(activeBoardCallId, assignment.ResourceAssignmentId, '')));

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

moveResourceAssignment is an external service call invoked without a try/catch boundary, so network or service-layer failures during batch moves bubble up without contextual error mapping. Wrap the batch in try/catch with structured context (activeBoardCallId, nodeId, count) and map the exception to an application-level error or user notification.

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

Prompt for LLM

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

Line 490:

moveResourceAssignment is an external service call invoked without a try/catch boundary, so network or service-layer failures during batch moves bubble up without contextual error mapping. Wrap the batch in try/catch with structured context (activeBoardCallId, nodeId, count) and map the exception to an application-level error or user notification.

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)/command.tsx
}
const laneAssignments = (boards[activeBoardCallId]?.board?.Assignments ?? []).filter((a) => !a.ReleasedOn && a.CommandStructureNodeId === nodeId);
if (disposition === 'pool') {
const outcomes = await Promise.all(laneAssignments.map((assignment) => moveResourceAssignment(activeBoardCallId, assignment.ResourceAssignmentId, '')));

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

Promise.all short-circuits on the first rejection, causing remaining assignments in the batch of moveResourceAssignment operations to be unhandled and losing per-item failure context. Replace with Promise.allSettled and inspect each settled status individually rather than relying on a single rejection.

Kody rule violation: Use Promise.allSettled for batch operations with partial failures

Prompt for LLM

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

Line 490:

Promise.all short-circuits on the first rejection, causing remaining assignments in the batch of moveResourceAssignment operations to be unhandled and losing per-item failure context. Replace with Promise.allSettled and inspect each settled status individually rather than relying on a single rejection.

Talk to Kody by mentioning @kody

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

const rawColor = first(params.color);
const color = rawColor && /^#[0-9a-fA-F]{3,8}$/.test(rawColor) ? rawColor : '#E53E3E';

const isValid = Number.isFinite(latitude) && Number.isFinite(longitude) && latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180 && (latitude !== 0 || longitude !== 0);

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

Inline magic numbers representing geographic coordinate bounds (-90, 90, -180, 180) reduce readability and make bounds changes error-prone. Extract named constants (MIN_LATITUDE, MAX_LATITUDE, MIN_LONGITUDE, MAX_LONGITUDE) at module level and reference them in the validation expression.

Kody rule violation: Replace magic numbers with named constants

Prompt for LLM

File src/app/resource-map/index.tsx:

Line 28:

Inline magic numbers representing geographic coordinate bounds (-90, 90, -180, 180) reduce readability and make bounds changes error-prone. Extract named constants (MIN_LATITUDE, MAX_LATITUDE, MIN_LONGITUDE, MAX_LONGITUDE) at module level and reference them in the validation expression.

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 Jul 28, 2026

Copy link
Copy Markdown

Kody Review Complete

Great news! 🎉
No issues were found that match your current review configurations.

Keep up the excellent work! 🚀

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 merged commit 5fd1386 into master Jul 28, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants