Skip to content

Develop - #124

Merged
ucswift merged 5 commits into
masterfrom
develop
Aug 10, 2026
Merged

ucswift merged 5 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Aug 10, 2026

Copy link
Copy Markdown
Member

Pull Request Description

This PR addresses several critical bugs in the chat system, fixes a startup race condition, and includes various robustness improvements.

Key Changes

Chat Realtime Connectivity Fixes (SignalR)

  • Hub invocation argument counts: JoinChannel, Typing, and MarkRead were sending fewer arguments than the hub methods declare, causing SignalR to reject the invocations and leaving the client permanently outside its channel groups (silent channels). All three now pass the full argument set including asUnitId.
  • Reconnect re-arming: When the SignalR transport reconnects, it gets a new connection ID that belongs to no channel groups. The client now detects reconnects and re-invokes Connect to re-enter channel groups, with retry logic (up to 3 attempts) and debounced resync.
  • Multi-argument hub events: The event handler system now forwards all positional arguments from hub methods (e.g., chatPresenceChanged sends userId, isOnline as separate args instead of an object).
  • Message collection normalization: Realtime payloads that omit empty Reactions/Attachments arrays are now normalized on the way in, and existing collections are preserved during partial updates so reactions are never dropped from the UI.

App Initialization Guard

  • Added a session generation token to prevent a stale initialization run (from a signed-out session) from connecting hubs, marking the app initialized, or restarting location tracking after sign-out cleanup has already run.

Chat Metadata Wire Format

  • GIF and location metadata now uses a nested camelCase envelope ({ location: { latitude, longitude } }) matching the web client's contract, replacing the flat PascalCase format the web client couldn't parse. Parsers accept both formats for backward compatibility with existing server history.

UI Fixes

  • Removed the edit action from chatbot/assistant messages (not applicable to AI messages).
  • Thread reply composer now hides image and GIF buttons instead of showing non-functional actions that silently discarded chosen photos.
  • Added null-safety for Reactions and Attachments on messages predating collection normalization.
  • Removed avatar initials fallback (the avatar endpoint always returns a silhouette placeholder).
  • Disabled Sentry debug logging in development to stop console flooding from watchdog-termination tracking.

Tests Added

  • Initialization session generation guard tests
  • Message composer conditional attachment button tests
  • SignalR hub invocation argument count and message normalization tests

ucswift and others added 5 commits August 9, 2026 20:24
Signing out while initializeApp was still awaiting left the stale run free
to connect the SignalR hubs and mark the app initialized for a session that
had already ended.

Capture a generation token at the start of each run, bump it on sign-out,
and bail at every checkpoint that is no longer current. The init promise's
finally handler is guarded too, so a run that lost the timeout race can no
longer clear the in-progress guard a newer run owns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X8YKbDjQeSLXJs4kU1qdXe
@Resgrid-Bot

Resgrid-Bot commented Aug 10, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

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

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

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

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

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR guards app initialization by session generation, expands SignalR argument and lifecycle handling, adds chat connection recovery, normalizes chat metadata, and limits unsupported chat actions. It also adds tests for initialization, composer behavior, and chat hub payloads.

Changes

Session initialization

Layer / File(s) Summary
Generation-guarded initialization
src/app/(app)/_layout.tsx, src/app/(app)/__tests__/init-session-generation.test.tsx
Initialization invalidates stale runs after sign-out and tests active, abandoned, and overlapping sessions.

SignalR chat realtime

Layer / File(s) Summary
Variadic SignalR event contract
src/services/signalr.service.ts
Listeners and dispatch now preserve positional arguments and emit hub lifecycle events.
Chat connection arming and recovery
src/stores/signalr/signalr-store.ts
Chat connections use serialized arming, retries, heartbeat restart, debounced resynchronization, and disconnect cleanup.
Chat hub payloads and invocations
src/stores/chat/store.ts, src/stores/chat/__tests__/hub-invoke-args.test.ts
Chat handling supports positional payloads and casing variants. Hub invocations include required arguments. Message collections are preserved when omitted.

Chat UI and metadata

Layer / File(s) Summary
Chat metadata normalization
src/components/chat/chat-utils.ts, src/app/chat/[channelId].tsx, src/app/chat/thread/[messageId].tsx
Shared builders serialize location and GIF metadata. Parsers validate nested and legacy formats.
Conditional composer actions
src/components/chat/message-composer.tsx, src/app/chat/thread/[messageId].tsx, src/components/chat/__tests__/message-composer.test.tsx
Image and GIF actions render only when supported callbacks exist. Tests cover action visibility and GIF callbacks.
Message actions and presentation
src/components/chat/message-actions-sheet.tsx, src/components/chat/message-bubble.tsx, src/components/chat/new-conversation-sheet.tsx, src/app/(app)/chatbot.tsx
Assistant messages no longer expose unsupported actions. Avatars and message collections use defensive defaults.

Sentry logging

Layer / File(s) Summary
Explicit Sentry debug control
src/app/_layout.tsx
Sentry debug logging now uses an explicit disabled constant.

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

Sequence Diagram(s)

sequenceDiagram
  participant SignalRService
  participant signalr-store
  participant ChatState
  SignalRService->>signalr-store: emit reconnect or disconnect
  signalr-store->>SignalRService: arm chat connection and invoke Connect
  SignalRService-->>signalr-store: forward positional hub arguments
  signalr-store->>ChatState: resynchronize chat state
  signalr-store->>signalr-store: restart heartbeat after connection
Loading

Possibly related PRs

Suggested reviewers: github-actions

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.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 "Develop" is too generic and does not identify the primary changes to initialization, chat messaging, and SignalR handling. Replace "Develop" with a concise title that identifies the main change, such as "Harden chat SignalR handling and session initialization".
✅ 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 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

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

❤️ Share

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

@ucswift

ucswift commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

Approve

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This PR is approved.

@ucswift
ucswift merged commit df24b8b into master Aug 10, 2026
10 of 12 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
src/services/signalr.service.ts (1)

418-426: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Emit a reconnected lifecycle event after fallback reconnection.

SignalRService.HUB_RECONNECTED_EVENT is only emitted from onreconnected. handleConnectionClose creates and starts a new connection, but _connectToHubInternal and connectToHubWithEventingUrl only emit HUB_DISCONNECTED_EVENT after the prior session closes.

When the chat store receives that restart path, it skips re-arming because isChatHubConnected is still true and connectChatHub exits at line 727. Track when a fresh connection replaces a disconnected connection, then emit the reconnected lifecycle event after connection.start() for that case so chat can call Connect for the new session.

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

In `@src/services/signalr.service.ts` around lines 418 - 426, Track whether
fallback reconnection in handleConnectionClose creates a replacement for a
previously disconnected session, carrying that state through
_connectToHubInternal and connectToHubWithEventingUrl. After the replacement
connection successfully completes connection.start(), emit
SignalRService.HUB_RECONNECTED_EVENT for the hub, while preserving the existing
HUB_DISCONNECTED_EVENT and onreconnected behavior for their respective paths.
🧹 Nitpick comments (1)
src/app/chat/thread/[messageId].tsx (1)

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

Use a stable onTyping callback.

onTyping={() => undefined} creates a new callback on every ThreadScreen render. MessageComposer uses onTyping in callback dependencies. Define a memoized no-op callback before rendering, then pass that callback to MessageComposer.

As per coding guidelines, avoid anonymous functions in event handlers to prevent re-renders.

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

In `@src/app/chat/thread/`[messageId].tsx at line 133, Define a memoized no-op
onTyping callback in ThreadScreen before the JSX render, then pass that stable
callback to MessageComposer instead of the inline arrow function. Keep the
callback behavior unchanged.

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 `@src/app/`(app)/__tests__/init-session-generation.test.tsx:
- Around line 70-92: Add stale-session coverage for every await boundary in the
initialization flow exercised by useInitGuard and the layout: gate each stage
with deferred promises, sign out immediately after each gate resolves, then
assert the next stage and all subsequent effects are not called. Extend the
existing “abandons an in-flight run” test structure without changing the valid
active-session path.

In `@src/app/`(app)/_layout.tsx:
- Around line 171-172: In src/app/(app)/_layout.tsx#L171-L172, update the
session initialization flow to check isCurrentRun() after every awaited
initialization stage and before each subsequent session-scoped operation,
returning immediately when the run is stale. In
src/app/(app)/__tests__/init-session-generation.test.tsx#L70-L92, add deferred
gates for each initialization stage and assert that signing out prevents all
later stages from executing.
- Around line 374-379: Update the status === 'signedOut' cleanup path in the app
layout to also reset hasInitialized.current to false, alongside
initGeneration.current and isInitializing.current. Preserve the existing
in-flight initialization invalidation so the next signed-in session can
initialize normally.

In `@src/components/chat/chat-utils.ts`:
- Around line 96-99: Update parseLocationMetadata to validate parsed latitude
and longitude ranges before returning the location: latitude must be between -90
and 90 inclusive, and longitude between -180 and 180 inclusive. Return null for
coordinates outside those bounds while preserving the existing handling of
missing values and valid metadata.
- Around line 91-115: Add Jest tests for parseLocationMetadata and
parseGifMetadata covering nested and legacy flat payloads, malformed JSON,
invalid or missing location coordinates, and GIF metadata with optional fields
present or absent. Assert normalized output values and null results for invalid
inputs, using the existing test conventions and helpers.

In `@src/stores/signalr/signalr-store.ts`:
- Around line 89-115: Add an arm-operation generation or cancellation token
shared by runChatArm, armChatSession, and disconnectChatHub, and invalidate it
when disconnecting. In runChatArm, verify the operation is still current after
Connect resolves or rejects before scheduling retries, starting the heartbeat,
or invoking resyncChat; ensure retry timer callbacks handle the returned promise
without unhandled rejections. Preserve normal retry behavior for the active
connection generation.

---

Outside diff comments:
In `@src/services/signalr.service.ts`:
- Around line 418-426: Track whether fallback reconnection in
handleConnectionClose creates a replacement for a previously disconnected
session, carrying that state through _connectToHubInternal and
connectToHubWithEventingUrl. After the replacement connection successfully
completes connection.start(), emit SignalRService.HUB_RECONNECTED_EVENT for the
hub, while preserving the existing HUB_DISCONNECTED_EVENT and onreconnected
behavior for their respective paths.

---

Nitpick comments:
In `@src/app/chat/thread/`[messageId].tsx:
- Line 133: Define a memoized no-op onTyping callback in ThreadScreen before the
JSX render, then pass that stable callback to MessageComposer instead of the
inline arrow function. Keep the callback behavior unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4e1812c6-a2ef-4854-82ca-b8bc9df9c839

📥 Commits

Reviewing files that changed from the base of the PR and between 945dc9b and c772902.

📒 Files selected for processing (16)
  • src/app/(app)/__tests__/init-session-generation.test.tsx
  • src/app/(app)/_layout.tsx
  • src/app/(app)/chatbot.tsx
  • src/app/_layout.tsx
  • src/app/chat/[channelId].tsx
  • src/app/chat/thread/[messageId].tsx
  • src/components/chat/__tests__/message-composer.test.tsx
  • src/components/chat/chat-utils.ts
  • src/components/chat/message-actions-sheet.tsx
  • src/components/chat/message-bubble.tsx
  • src/components/chat/message-composer.tsx
  • src/components/chat/new-conversation-sheet.tsx
  • src/services/signalr.service.ts
  • src/stores/chat/__tests__/hub-invoke-args.test.ts
  • src/stores/chat/store.ts
  • src/stores/signalr/signalr-store.ts

Comment on lines +70 to +92
it('abandons an in-flight run when the session ends mid-initialization', async () => {
const gate = deferred();
const { result } = renderHook(() => useInitGuard(gate, effects));

let pending: Promise<void> = Promise.resolve();
act(() => {
pending = result.current.initialize();
});

// Sign-out lands while initialization is still awaiting its first step.
act(() => {
result.current.signOut();
});

await act(async () => {
gate.resolve();
await pending;
});

expect(effects.connectHub).not.toHaveBeenCalled();
expect(effects.markInitialized).not.toHaveBeenCalled();
expect(effects.startLocation).not.toHaveBeenCalled();
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add a stale-session test for each initialization stage.

This helper has one await before connectHub. The layout has four awaits before its first generation check. The test therefore stays green when a stale layout run continues from core initialization into calls, rights, or feature-flag initialization.

Use deferred gates for each stage. Sign out after each gate resolves. Assert that the next stage and all later effects do not run.

As per coding guidelines, “Generate tests for all components, services and logic generated. Ensure tests run without errors and fix any issues.”

🤖 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)/__tests__/init-session-generation.test.tsx around lines 70 -
92, Add stale-session coverage for every await boundary in the initialization
flow exercised by useInitGuard and the layout: gate each stage with deferred
promises, sign out immediately after each gate resolves, then assert the next
stage and all subsequent effects are not called. Extend the existing “abandons
an in-flight run” test structure without changing the valid active-session path.

Source: Coding guidelines

Comment thread src/app/(app)/_layout.tsx
Comment on lines +171 to +172
if (!isCurrentRun()) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Guard and test every initialization stage. The first generation check occurs after several awaited operations, and the test model has only one awaited operation. A sign-out between production stages can therefore run stale work without a failing test.

  • src/app/(app)/_layout.tsx#L171-L172: check isCurrentRun() after each awaited initialization stage and before the next session-scoped operation.
  • src/app/(app)/__tests__/init-session-generation.test.tsx#L70-L92: add deferred stage gates and assert that sign-out prevents every subsequent stage.
📍 Affects 2 files
  • src/app/(app)/_layout.tsx#L171-L172 (this comment)
  • src/app/(app)/__tests__/init-session-generation.test.tsx#L70-L92
🤖 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)/_layout.tsx around lines 171 - 172, In
src/app/(app)/_layout.tsx#L171-L172, update the session initialization flow to
check isCurrentRun() after every awaited initialization stage and before each
subsequent session-scoped operation, returning immediately when the run is
stale. In src/app/(app)/__tests__/init-session-generation.test.tsx#L70-L92, add
deferred gates for each initialization stage and assert that signing out
prevents all later stages from executing.

Source: Coding guidelines

Comment thread src/app/(app)/_layout.tsx
Comment on lines +374 to +379

// Retire any initialization still in flight so it cannot connect hubs or mark the
// app initialized for a session that ended, and free the guard it no longer owns
// so the next sign-in is not skipped as "already initializing".
initGeneration.current += 1;
isInitializing.current = false;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find auth-status transitions and navigation behavior around sign-out.
rg -n -C 5 "signedOut|signOut|set.*status|status: 'idle'|status: 'error'" src
rg -n -C 5 "hasInitialized\.current|initializeApp\(" "src/app/(app)/_layout.tsx"

Repository: Resgrid/Dispatch

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'src/app/\(app\)/_layout\.tsx|src/(app|modules|screens|components).*sign|src/.*auth|src/.*coreStore|init-session-generation\.test\.tsx' | head -200

echo
echo "== layout outline relevant =="
wc -l "src/app/(app)/_layout.tsx"
ast-grep outline "src/app/(app)/_layout.tsx" --match core --view expanded || true

echo
echo "== layout lines 280-385 =="
sed -n '280,385p' "src/app/(app)/_layout.tsx"

echo
echo "== init-session test =="
sed -n '1,180p' "src/app/(app)/__tests__/init-session-generation.test.tsx"

echo
echo "== focused signOut/status/hasInitialized references =="
rg -n -C 3 "signOut|signedOut|setSignedOut|hasInitialized\.current|initializeApp\(" "src/app/(app)/_layout.tsx" "src/app/(app)/__tests__/init-session-generation.test.tsx"

Repository: Resgrid/Dispatch

Length of output: 16913


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== src/app/(app)/_layout.tsx first 450 lines =="
sed -n '1,450p' "src/app/(app)/_layout.tsx"

echo "== auth state/status references in src =="
rg -n -C 3 "setStatus|signOut|signedOut|signedIn|idle|error" src/lib/auth src/stores src/app src/modules src/screens src/components | head -500

Repository: Resgrid/Dispatch

Length of output: 46670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== auth store outline =="
wc -l src/lib/auth/index.tsx src/stores/auth/store.tsx src/app/_layout.tsx src/__tests__/app/_layout.auth-guard.test.tsx
ast-grep outline src/stores/auth/store.tsx --view expanded || true
ast-grep outline src/lib/auth/index.tsx --view expanded || true

echo
echo "== auth store =="
sed -n '1,240p' src/stores/auth/store.tsx

echo
echo "== auth index =="
sed -n '1,220p' src/lib/auth/index.tsx

echo
echo "== root layout auth guard slices =="
sed -n '1,260p' src/app/_layout.tsx
sed -n '1,180p' src/__tests__/app/_layout.auth-guard.test.tsx

echo
echo "== focused redirect/log-out usages =="
rg -n -C 4 "useAuthStore|useAuth|login\\(|logout\\(|setStatus\\(|signedOut|redirectTo:|router\\." src/app src/lib/auth src/__tests__ src/components src/hooks | head -400

Repository: Resgrid/Dispatch

Length of output: 48462


Reset the completed-session initialization flag on sign-out.

layout only resets isInitializing on sign-out, while a completed run leaves hasInitialized.current === true. A kept-mounted (app) layout will skip the next signed-in session initialization. Add hasInitialized.current = false in the status === 'signedOut' cleanup path.

🤖 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)/_layout.tsx around lines 374 - 379, Update the status ===
'signedOut' cleanup path in the app layout to also reset hasInitialized.current
to false, alongside initGeneration.current and isInitializing.current. Preserve
the existing in-flight initialization invalidation so the next signed-in session
can initialize normally.

Comment on lines 91 to +115
export function parseLocationMetadata(metadataJson?: string | null): ChatLocationMetadata | null {
return parseMetadata<ChatLocationMetadata>(metadataJson);
const raw = parseMetadata<Record<string, unknown>>(metadataJson);
if (!raw) return null;
const nested = (raw.location ?? raw.Location) as Record<string, unknown> | undefined;
const source = nested ?? raw;
const latitude = readNumber(source.latitude ?? source.Latitude);
const longitude = readNumber(source.longitude ?? source.Longitude);
if (latitude === undefined || longitude === undefined) return null;
return { Latitude: latitude, Longitude: longitude, Label: readString(source.label ?? source.Label) };
}

export function parseGifMetadata(metadataJson?: string | null): ChatGifMetadata | null {
return parseMetadata<ChatGifMetadata>(metadataJson);
const raw = parseMetadata<Record<string, unknown>>(metadataJson);
if (!raw) return null;
const nested = (raw.gif ?? raw.Gif) as Record<string, unknown> | undefined;
const source = nested ?? raw;
const url = readString(source.url ?? source.Url ?? source.gifUrl ?? source.GifUrl);
if (!url) return null;
return {
GifUrl: url,
PreviewUrl: readString(source.previewUrl ?? source.PreviewUrl),
Width: readNumber(source.width ?? source.Width),
Height: readNumber(source.height ?? source.Height),
Title: readString(source.title ?? source.Title),
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline src/components/chat/chat-utils.ts --items all
rg -n -C 4 'parseLocationMetadata|parseGifMetadata|buildLocationMetadata|buildGifMetadata' \
  src/components/chat/__tests__/chat-utils.test.ts

Repository: Resgrid/Dispatch

Length of output: 2266


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Test file existence and size:"
if [ -f src/components/chat/__tests__/chat-utils.test.ts ]; then
  wc -l src/components/chat/__tests__/chat-utils.test.ts
  echo
  echo "Relevant test imports/descriptions/expectations:"
  rg -n -C 3 'describe\(|it\(|test\(|expect\(|parseLocationMetadata|parseGifMetadata|buildLocationMetadata|buildGifMetadata|metadata' \
    src/components/chat/__tests__/chat-utils.test.ts || true
else
  echo "NotFound src/components/chat/__tests__/chat-utils.test.ts"
fi

echo
echo "Parsed metadata implementation:"
sed -n '55,120p' src/components/chat/__tests__/chat-utils.test.ts 2>/dev/null || true
sed -n '55,120p' src/components/chat/chat-utils.ts

Repository: Resgrid/Dispatch

Length of output: 11561


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Tests for parseLocationMetadata/parseGifMetadata/importers:"
rg -n 'parseLocationMetadata|parseGifMetadata|chat-location|gif-metadata|LocationMetadata|GifMetadata' src || true

echo
echo "Jest coverage config files:"
for f in package.json jest.config.js jest.config.ts jest.config.json babel.config.js jest.setup.js; do
  if [ -f "$f" ]; then
    echo "--- $f"
    sed -n '1,220p' "$f"
  fi
done

Repository: Resgrid/Dispatch

Length of output: 13715


Add Jest coverage for metadata normalization.

Add tests for parseLocationMetadata and parseGifMetadata covering nested payloads, legacy flat payloads, malformed JSON, invalid coordinates, and optional GIF fields.

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

In `@src/components/chat/chat-utils.ts` around lines 91 - 115, Add Jest tests for
parseLocationMetadata and parseGifMetadata covering nested and legacy flat
payloads, malformed JSON, invalid or missing location coordinates, and GIF
metadata with optional fields present or absent. Assert normalized output values
and null results for invalid inputs, using the existing test conventions and
helpers.

Source: Coding guidelines

Comment on lines +96 to +99
const latitude = readNumber(source.latitude ?? source.Latitude);
const longitude = readNumber(source.longitude ?? source.Longitude);
if (latitude === undefined || longitude === undefined) return null;
return { Latitude: latitude, Longitude: longitude, Label: readString(source.label ?? source.Label) };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject coordinates outside valid geographic ranges.

readNumber accepts values such as latitude 91 and longitude 181. parseLocationMetadata then returns them, and MessageBubble uses them in a map URL. Reject latitude outside -90..90 and longitude outside -180..180.

Proposed fix
   const latitude = readNumber(source.latitude ?? source.Latitude);
   const longitude = readNumber(source.longitude ?? source.Longitude);
-  if (latitude === undefined || longitude === undefined) return null;
+  if (latitude === undefined || longitude === undefined || latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) return null;
   return { Latitude: latitude, Longitude: longitude, Label: readString(source.label ?? source.Label) };
📝 Committable suggestion

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

Suggested change
const latitude = readNumber(source.latitude ?? source.Latitude);
const longitude = readNumber(source.longitude ?? source.Longitude);
if (latitude === undefined || longitude === undefined) return null;
return { Latitude: latitude, Longitude: longitude, Label: readString(source.label ?? source.Label) };
const latitude = readNumber(source.latitude ?? source.Latitude);
const longitude = readNumber(source.longitude ?? source.Longitude);
if (
latitude === undefined ||
longitude === undefined ||
latitude < -90 ||
latitude > 90 ||
longitude < -180 ||
longitude > 180
) return null;
return { Latitude: latitude, Longitude: longitude, Label: readString(source.label ?? source.Label) };
🤖 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/chat/chat-utils.ts` around lines 96 - 99, Update
parseLocationMetadata to validate parsed latitude and longitude ranges before
returning the location: latitude must be between -90 and 90 inclusive, and
longitude between -180 and 180 inclusive. Return null for coordinates outside
those bounds while preserving the existing handling of missing values and valid
metadata.

Comment on lines +89 to +115
async function runChatArm(): Promise<void> {
stopChatArmRetry();

try {
await signalRService.invoke(Env.CHAT_HUB_NAME, 'Connect');
} catch (error) {
chatArmAttempts += 1;
logger.warn({
message: 'Failed to announce presence to chat hub',
context: { error, attempt: chatArmAttempts, maxAttempts: CHAT_ARM_MAX_ATTEMPTS },
});
if (chatArmAttempts < CHAT_ARM_MAX_ATTEMPTS) {
chatArmRetryTimer = setTimeout(() => {
void armChatSession();
}, CHAT_ARM_RETRY_MS);
}
throw error;
}

chatArmAttempts = 0;

stopChatHeartbeat();
chatHeartbeatTimer = setInterval(() => {
signalRService.invoke(Env.CHAT_HUB_NAME, 'Heartbeat').catch(() => {
// Heartbeat is best-effort; ignore transient failures.
});
}, CHAT_HEARTBEAT_INTERVAL_MS);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find file:"
fd -a 'signalr-store\.ts$' . || true

echo "Outline:"
file="$(fd 'signalr-store\.ts$' . | head -n1)"
if [ -n "${file:-}" ]; then
  wc -l "$file"
  ast-grep outline "$file" --view compact || true
  echo "Relevant excerpts:"
  sed -n '1,180p' "$file" | cat -n
  echo "Lines 780-845:"
  sed -n '780,845p' "$file" | cat -n
  echo "Search disconnect/chatArm symbols:"
  rg -n "disconnectChatHub|runChatArm|armChatSession|chatHeartbeatTimer|chatArmRetryTimer|chatArmAttempts|resync|sync" "$file"
fi

Repository: Resgrid/Dispatch

Length of output: 12208


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="$(fd 'signalr-store\.ts$' . | head -n1)"
if [ -n "${file:-}" ]; then
  echo "Diff context around store references:"
  git diff -- "$file" | sed -n '1,260p' || true
fi

echo "SignalR service invoke type/usages:"
rg -n "signalRService|invoke\(|class SignalR|interface .*SignalR|connectChatHub|disconnectChatHub" . -g '*.ts' -g '*.tsx'

Repository: Resgrid/Dispatch

Length of output: 26060


Invalidate in-flight arm operations during disconnect.

disconnectChatHub stops the retry timer, but an armChatSession promise that is already awaiting signalRService.invoke(Env.CHAT_HUB_NAME, 'Connect') can still complete after disconnect. Use a cancellation token or generation check: clear the pending arm during disconnect, have runChatArm cancel or return before scheduling a retry, starting the heartbeat, or calling resyncChat, and handle the retry promise in the timer callback.

[st2021_and_safety_of_operation]

🤖 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 89 - 115, Add an
arm-operation generation or cancellation token shared by runChatArm,
armChatSession, and disconnectChatHub, and invalidate it when disconnecting. In
runChatArm, verify the operation is still current after Connect resolves or
rejects before scheduling retries, starting the heartbeat, or invoking
resyncChat; ensure retry timer callbacks handle the returned promise without
unhandled rejections. Preserve normal retry behavior for the active connection
generation.


await act(async () => {
first.resolve();
await Promise.all([pending, second]);

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

Batch operation failure risk: await Promise.all([pending, second]) causes a single rejection to fail the entire batch. Replace it with Promise.allSettled to properly handle per-item results for independent tasks.

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

Prompt for LLM

File src/app/(app)/__tests__/init-session-generation.test.tsx:

Line 133:

Batch operation failure risk: `await Promise.all([pending, second])` causes a single rejection to fail the entire batch. Replace it with `Promise.allSettled` to properly handle per-item results for independent tasks.

Talk to Kody by mentioning @kody

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

Comment thread src/app/(app)/_layout.tsx
Comment on lines +378 to +379
initGeneration.current += 1;
isInitializing.current = false;

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 Bug high

State management bug: The sign-out handler resets isInitializing.current but not hasInitialized.current, latching the shouldInitialize gate (line 344) true and skipping initialization for the next session's SignalR hubs, feature flags, and weather alerts. Reset hasInitialized.current = false; alongside the isInitializing.current = false; assignment.

// Retire any initialization still in flight so it cannot connect hubs or mark the
// app initialized for a session that ended, and free the guard it no longer owns
// so the next sign-in is not skipped as "already initializing".
initGeneration.current += 1;
isInitializing.current = false;
hasInitialized.current = false;
Prompt for LLM

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

Line 378 to 379:

State management bug: The sign-out handler resets `isInitializing.current` but not `hasInitialized.current`, latching the `shouldInitialize` gate (line 344) true and skipping initialization for the next session's SignalR hubs, feature flags, and weather alerts. Reset `hasInitialized.current = false;` alongside the `isInitializing.current = false;` assignment.

Suggested Code:

      // Retire any initialization still in flight so it cannot connect hubs or mark the
      // app initialized for a session that ended, and free the guard it no longer owns
      // so the next sign-in is not skipped as "already initializing".
      initGeneration.current += 1;
      isInitializing.current = false;
      hasInitialized.current = false;

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)/chatbot.tsx
setEditMessage(m);
setEditText(m.Body ?? '');
}}
onEdit={() => undefined}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Performance regression: Inline arrow functions in JSX props create new functions on every render. Move the () => undefined callback definition outside the render method, including the instance in src/app/chat/thread/[messageId].tsx:133.

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

Prompt for LLM

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

Line 159:

Performance regression: Inline arrow functions in JSX props create new functions on every render. Move the `() => undefined` callback definition outside the render method, including the instance in `src/app/chat/thread/[messageId].tsx:133`.

Talk to Kody by mentioning @kody

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

});
if (chatArmAttempts < CHAT_ARM_MAX_ATTEMPTS) {
chatArmRetryTimer = setTimeout(() => {
void armChatSession();

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: The retry timer fires void armChatSession() without a .catch handler, discarding the rejected Promise when runChatArm rethrows an invoke error and potentially crashing Node ≥15 processes. Attach .catch(() => { /* runChatArm already logged and rescheduled the retry */ }) to the armChatSession() call inside the setTimeout callback.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

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

Line 102:

Unhandled Promise rejection: The retry timer fires `void armChatSession()` without a `.catch` handler, discarding the rejected Promise when `runChatArm` rethrows an invoke error and potentially crashing Node ≥15 processes. Attach `.catch(() => { /* runChatArm already logged and rescheduled the retry */ })` to the `armChatSession()` call inside the `setTimeout` callback.

Talk to Kody by mentioning @kody

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

* every reconnect issues a fresh connection id. Without re-arming, the websocket stays
* open but the client receives nothing.
*/
async function runChatArm(): Promise<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

Missing JSDoc annotations: The runChatArm JSDoc block (lines 82–88) violates Rule 22 by omitting @returns {Promise<void>} and @throws for the async function. Add these tags to document the rejection when signalRService.invoke('Connect') fails after logging and scheduling a retry, and apply similarly at line 128.

Kody rule violation: Document async/Promise behavior and errors

Prompt for LLM

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

Line 89:

Missing JSDoc annotations: The `runChatArm` JSDoc block (lines 82–88) violates Rule 22 by omitting `@returns {Promise<void>}` and `@throws` for the async function. Add these tags to document the rejection when `signalRService.invoke('Connect')` fails after logging and scheduling a retry, and apply similarly at line 128.

Talk to Kody by mentioning @kody

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

@coderabbitai coderabbitai Bot mentioned this pull request Aug 11, 2026
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