Skip to content

RU-T50 Bug and Security fixes, permission fix - #256

Merged
ucswift merged 2 commits into
masterfrom
develop
Jul 26, 2026
Merged

RU-T50 Bug and Security fixes, permission fix#256
ucswift merged 2 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Jul 25, 2026

Copy link
Copy Markdown
Member

Summary

This PR delivers a comprehensive set of security hardening, bug fixes, and performance optimizations for the Resgrid Unit app (RU-T50).

Security Fixes

  • SAML Login CSRF Protection: Added a RelayState nonce to the SAML SSO flow. Callbacks are validated against a one-time nonce so attackers cannot inject a forged SAML response on a victim's device.
  • SignalR Token Leak Prevention: Removed bearer tokens from the geolocation hub's URL query string (previously visible in server/proxy access logs) and moved authentication to accessTokenFactory.
  • Electron Link Security: External link opening is now restricted to http(s) URLs only, blocking dangerous schemes (file:, javascript:, custom protocols).
  • HTML Renderer Sandboxing: Removed allow-same-origin from the iframe sandbox so server-supplied HTML runs on an opaque origin with no access to app storage/cookies. Added URL scheme validation on link clicks.
  • Video WebView Hardening: Feed URLs are validated as http(s) before loading; JavaScript is disabled for MJPEG/other formats that don't require it.
  • Android Permissions: Blocked READ_MEDIA_IMAGES and READ_MEDIA_VIDEO permissions by switching to the system photo picker (selected-item-only access).
  • HTTPS-Only Server URLs: Production builds now reject plain HTTP server URLs to prevent cleartext token transmission.
  • Log Sanitization: Enhanced the logging sanitizer with substring-based sensitive key detection, axios error summarization (strips request bodies with credentials), URL query/hash stripping, and severity-based early bail. Removed username logging from auth API calls.

Bug Fixes

  • Concurrent Token Refresh Force-Logout: Introduced a single-flight refresh lock so all concurrent refresh triggers (401 interceptor, proactive timer, SignalR reconnect) share one request — preventing refresh-token rotation from invalidating parallel callers.
  • UTC Date Parsing: Fixed API timestamps lacking timezone designators being parsed as device-local time. Added parseApiUtcDate() and crash-safe safeFormatDate().
  • SignalR Reconnection: Rewrote reconnection with retry-until-cap semantics (failed attempts reschedule themselves), increased max attempts (5→10), linear backoff capped at 60s, and aborts when no token remains after refresh. Emits hub-scoped lifecycle events for group re-join and state resync.
  • Stale Active Unit/Call: Persists no longer existing unit/call IDs are now cleared on init instead of causing infinite loading.
  • Offline Queue Recovery: Events stuck in PROCESSING status after an app kill are recovered to PENDING on rehydration; duplicate NetInfo listeners are prevented.
  • Dispatch Cache Staleness: Call dispatches now have a 5-minute TTL so newly dispatched units appear; failed fetches are no longer cached as empty arrays.
  • Notes Cache Invalidation: Saving a note now invalidates the notes list cache so changes are immediately visible.
  • LiveKit Unexpected Disconnect: Added RoomEvent.Disconnected handling to reset call state and tear down CallKeep/foreground service when the room drops unexpectedly.
  • Complete Logout Data Wipe: All logout paths (manual, forced 401, refresh rejection) now run a full app-data teardown: SignalR disconnect, location stop, push unregister, all stores reset (including maps/pois/routes/weather/check-in-timers), and react-query cache cleared. Prevents cross-user data leakage on shared devices.
  • Server Switch Cache Isolation: Cache keys are now scoped by base URL and cleared on server URL change.

Performance Improvements

  • Static Map Images: Detail screens now use Mapbox Static Images API instead of full GL maps, significantly reducing memory and improving load times.
  • Parallelized App Init: Independent store fetches and SignalR hub connections now run in parallel instead of serially.
  • Memoized List Rendering: Calls, contacts, notes, protocols, and weather alerts lists use memoized filter/render logic and React.memo cards.
  • Camera Follow Throttle: Programmatic map camera updates throttled to 5-second intervals.
  • Shared Countdown Ticker: All check-in timer cards share a single 1-second interval instead of each running their own.
  • Notification Countdown: Check-in notification updates reduced from every 1s to every 15s to save battery.
  • Selective Store Subscriptions: Dispatch modal and status bottom sheet use targeted selectors instead of whole-store subscriptions to avoid unnecessary re-renders.
  • Background Polling Suppression: Route progress and check-in timer polling skip while the app is backgrounded.
  • Reduced Persistence Payload: Core and auth stores persist only essential selection IDs and credentials, eliminating heavy synchronous MMKV writes on every state change.

Summary by CodeRabbit

  • New Features
    • Open call locations in a full-screen map view.
    • Network-related location and status updates are queued for offline processing.
    • SAML sign-in callbacks now use enhanced validation.
  • Bug Fixes
    • Newly saved notes appear immediately.
    • Date and time displays handle invalid or missing values more reliably.
    • Background polling and map updates are reduced to improve responsiveness.
  • Security
    • Unsafe external links, embedded URLs, and non-secure server URLs are blocked.
    • Logout now fully clears cached data and active services.

@gitguardian

gitguardian Bot commented Jul 25, 2026

Copy link
Copy Markdown

️✅ There are no secrets present in this pull request anymore.

If these secrets were true positive and are still valid, we highly recommend you to revoke them.
While these secrets were previously flagged, we no longer have a reference to the
specific commits where they were detected. Once a secret has been leaked into a git
repository, you should consider it compromised, even if it was deleted immediately.
Find here more information about risks.


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR hardens authentication, external URL handling, logging, caching, SAML callbacks, realtime reconnection, offline queuing, app teardown, map rendering, timers, and list performance. It also adds or updates tests for these behaviors.

Changes

Application reliability and security

Layer / File(s) Summary
Authentication, logging, and caching
src/api/common/*, src/lib/auth/*, src/lib/logging/*, src/lib/cache/*
Token refresh uses single-flight coordination, logs carry structured trace context, sensitive values are sanitized, and cache entries are scoped, validated, pruned, and invalidated.
SAML and session cleanup
src/hooks/use-saml-login.ts, src/app/login/sso.tsx, src/services/app-reset.service.ts, src/app/_layout.tsx
SAML callbacks validate one-time RelayState nonces, and logout cleanup tears down services, clears stores, and clears React Query data.
Realtime and offline processing
src/services/signalr.service.ts, src/stores/signalr/*, src/hooks/use-*-signalr-updates.ts, src/services/location.ts
SignalR reconnects use managed timers and token factories; event timestamps, debounced status refreshes, and network-only offline queuing are added.
Application stores and lifecycle
src/stores/app/*, src/stores/auth/store.tsx, src/stores/calls/store.ts, src/stores/offline-queue/store.ts
Persisted selections, dispatch TTLs, LiveKit room transitions, background polling, and offline queue rehydration are updated.
Maps, media, and UI rendering
src/components/maps/*, src/app/call/[id].tsx, src/components/call-video-feeds/*, src/components/ui/html-renderer/*
Static maps, full-screen map interaction, safe web URLs, iframe sandboxing, and HTTP(S)/mailto link filtering are implemented.
Performance and timer behavior
src/app/(app)/*, src/components/check-in-timers/*, src/lib/shared-ticker.ts
List callbacks and derived data are memoized, shared ticker subscriptions replace per-card intervals, and notification countdowns use 15-second ticks.

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

Sequence Diagram(s)

sequenceDiagram
  participant ApiClient
  participant AuthStore
  participant RefreshLock
  participant API
  ApiClient->>AuthStore: handle 401 response
  AuthStore->>RefreshLock: refreshTokenSingleFlight
  RefreshLock->>API: refresh token request
  API-->>RefreshLock: AuthResponse
  RefreshLock-->>AuthStore: shared refresh result
  AuthStore-->>ApiClient: refreshed access token or failure
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 26.32% 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 related to the PR, but it is too vague to convey the main security, bug-fix, and performance changes. Use a more specific title that names the primary change, such as SAML CSRF hardening, SignalR security fixes, or app-wide logout/cache cleanup.
✅ 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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

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

❤️ Share

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

@Resgrid-Bot

This comment has been minimized.

Comment thread electron/main.js
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
require('electron').shell.openExternal(url);
if (/^https?:\/\//i.test(url)) {
require('electron').shell.openExternal(url);

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

Unguarded external OS call — shell.openExternal(url) is invoked without a try/catch wrapper, leaving missing-browser and permission-denied failures unhandled. Wrap the call in try/catch with context and log the url and err on failure (e.g., try { await require('electron').shell.openExternal(url); } catch (err) { console.error('openExternal failed', { url, err }); }).

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

Prompt for LLM

File electron/main.js:

Line 83:

Unguarded external OS call — shell.openExternal(url) is invoked without a try/catch wrapper, leaving missing-browser and permission-denied failures unhandled. Wrap the call in try/catch with context and log the url and err on failure (e.g., `try { await require('electron').shell.openExternal(url); } catch (err) { console.error('openExternal failed', { url, err }); }`).

Talk to Kody by mentioning @kody

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

Comment thread src/api/common/client.tsx
Comment on lines +114 to +117
logger.warn({
message: 'Request failed after token refresh attempt',
context: { error: refreshError },
});

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

Token-refresh failure log lacks the operation name and correlation identifiers required by Rule [3] for cross-log/SIEM correlation. Add structured fields for the operation and a correlation identifier, e.g., logger.warn({ message: 'Request failed after token refresh attempt', operation: 'token_refresh', trace_id: originalRequest.headers?.['x-trace-id'], context: { error: refreshError } });.

Kody rule violation: Include error context in structured logs

Prompt for LLM

File src/api/common/client.tsx:

Line 114 to 117:

Token-refresh failure log lacks the operation name and correlation identifiers required by Rule [3] for cross-log/SIEM correlation. Add structured fields for the operation and a correlation identifier, e.g., `logger.warn({ message: 'Request failed after token refresh attempt', operation: 'token_refresh', trace_id: originalRequest.headers?.['x-trace-id'], context: { error: refreshError } });`.

Talk to Kody by mentioning @kody

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

Comment thread src/api/notes/notes.ts
});
// The notes list is cached for 2 days — invalidate so the new/updated note
// is visible immediately instead of after cache expiry.
cacheManager.remove('/Notes/GetAllNotes');

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 cache key literal — '/Notes/GetAllNotes' is duplicated knowledge shared with the cache write site, so a typo in either location silently breaks cache invalidation. Define a shared constant (e.g., const NOTES_CACHE_KEY = '/Notes/GetAllNotes' in a routes/keys constants file) and reference it both here and wherever the notes list is cached.

Kody rule violation: Centralize string constants

Prompt for LLM

File src/api/notes/notes.ts:

Line 68:

Inline cache key literal — '/Notes/GetAllNotes' is duplicated knowledge shared with the cache write site, so a typo in either location silently breaks cache invalidation. Define a shared constant (e.g., `const NOTES_CACHE_KEY = '/Notes/GetAllNotes'` in a routes/keys constants file) and reference it both here and wherever the notes list is cached.

Talk to Kody by mentioning @kody

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

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

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Inline arrow function in the onPress JSX prop creates a new function on every render, impacting performance. Move the function definition outside the render method.

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

Prompt for LLM

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

Line 84:

Inline arrow function in the onPress JSX prop creates a new function on every render, impacting performance. Move the function definition outside the render method.

Talk to Kody by mentioning @kody

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

Comment thread src/app/(app)/protocols.tsx Outdated
}, [protocols, searchQuery]);

const renderProtocol = React.useCallback(({ item }: { item: (typeof filteredProtocols)[number] }) => <ProtocolCard protocol={item} onPress={handleProtocolPress} />, [handleProtocolPress]);
const protocolKeyExtractor = React.useCallback((item: (typeof filteredProtocols)[number], index: number) => item.ProtocolId || `protocol-${index}`, []);

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

Unstable list key — the key extractor falls back to a template literal embedding the array index when ProtocolId is absent, causing incorrect React reconciliation on reorder/insert/delete. Use a guaranteed unique identifier from the data or combine a stable field with another distinguishing property rather than the positional index.

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

Prompt for LLM

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

Line 62:

Unstable list key — the key extractor falls back to a template literal embedding the array index when ProtocolId is absent, causing incorrect React reconciliation on reorder/insert/delete. Use a guaranteed unique identifier from the data or combine a stable field with another distinguishing property rather than the positional index.

Talk to Kody by mentioning @kody

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

case FeedFormat.YouTubeLive:
case FeedFormat.Embed:
return safeWebUrl ? (
<WebView source={{ uri: safeWebUrl }} style={styles.video} allowsInlineMediaPlayback mediaPlaybackRequiresUserAction={false} javaScriptEnabled originWhitelist={['https://*', 'http://*']} />

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

Duplicated JSX — the safeWebUrl ? <WebView …> : <Text …> block repeats across the YouTubeLive/Embed, MJPEG/Other, and default switch cases with only minor prop differences, inviting copy-paste drift such as a missing originWhitelist. Extract a helper like renderSecureWebView({ javaScriptEnabled, mediaPlaybackRequiresUserAction }) that renders the WebView with shared props plus originWhitelist or falls back to the Text.

Kody rule violation: Extract duplicated logic into functions

Prompt for LLM

File src/components/call-video-feeds/video-player-modal.tsx:

Line 48:

Duplicated JSX — the `safeWebUrl ? <WebView …> : <Text …>` block repeats across the YouTubeLive/Embed, MJPEG/Other, and default switch cases with only minor prop differences, inviting copy-paste drift such as a missing originWhitelist. Extract a helper like `renderSecureWebView({ javaScriptEnabled, mediaPlaybackRequiresUserAction })` that renders the WebView with shared props plus originWhitelist or falls back to the Text.

Talk to Kody by mentioning @kody

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

}
const query = searchQuery.toLowerCase();
return {
users: data.users.filter((user) => user.Name.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 — user.Name is accessed and .toLowerCase() called without a null guard, throwing TypeError and crashing the modal when Name is undefined/null. Use optional chaining with a fallback (user?.Name?.toLowerCase()?.includes(query)) or pre-filter with user.Name != null.

Kody rule violation: Add null checks before accessing properties

Prompt for LLM

File src/components/calls/dispatch-selection-modal.tsx:

Line 51:

Null pointer dereference — user.Name is accessed and .toLowerCase() called without a null guard, throwing TypeError and crashing the modal when Name is undefined/null. Use optional chaining with a fallback (`user?.Name?.toLowerCase()?.includes(query)`) or pre-filter with `user.Name != null`.

Talk to Kody by mentioning @kody

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

useEffect(() => {
const interval = setInterval(() => {
// Shared ticker: all visible timer cards run on ONE interval.
return subscribeToSharedTicker(() => {

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 subscription error — subscribeToSharedTicker is invoked with only a tick callback and no onError/onFailure path, violating Rule [4]'s requirement that subscription/listener APIs always provide an error handler. Pass an error callback (e.g., subscribeToSharedTicker(onTick, { onError: (e) => logger.error('ticker failed', { err: e }) })) or document why errors are impossible for this ticker.

Kody rule violation: Provide error handlers to subscription/listener APIs

Prompt for LLM

File src/components/check-in-timers/check-in-timer-card.tsx:

Line 34:

Unhandled subscription error — subscribeToSharedTicker is invoked with only a tick callback and no onError/onFailure path, violating Rule [4]'s requirement that subscription/listener APIs always provide an error handler. Pass an error callback (e.g., `subscribeToSharedTicker(onTick, { onError: (e) => logger.error('ticker failed', { err: e }) })`) or document why errors are impossible for this ticker.

Talk to Kody by mentioning @kody

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

return null;
}

const styleId = colorScheme === 'dark' ? 'mapbox/dark-v11' : 'mapbox/streets-v12';

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 strings — the Mapbox style identifiers 'mapbox/dark-v11' and 'mapbox/streets-v12' are duplicated string literals where a typo silently breaks the map. Define a const map such as const MAP_STYLES = { dark: 'mapbox/dark-v11', street: 'mapbox/streets-v12' } as const; and reference it by key.

Kody rule violation: Use enums instead of magic strings

Prompt for LLM

File src/components/maps/static-map.tsx:

Line 36:

Inline magic strings — the Mapbox style identifiers 'mapbox/dark-v11' and 'mapbox/streets-v12' are duplicated string literals where a typo silently breaks the map. Define a const map such as `const MAP_STYLES = { dark: 'mapbox/dark-v11', street: 'mapbox/streets-v12' } as const;` and reference it by key.

Talk to Kody by mentioning @kody

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

{/* Show user location if requested */}
{showUserLocation && <Mapbox.UserLocation visible={true} showsUserHeadingIndicator={true} />}
</Mapbox.MapView>
<Image source={{ uri: imageUrl }} style={StyleSheet.flatten([styles.map, { height }])} resizeMode="cover" accessibilityLabel={address || 'Map'} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Hardcoded user-visible string — the fallback accessibility label 'Map' is untranslated despite useTranslation() being available (line 28) and t() used elsewhere (line 55), breaking i18n consistency for screen-reader users. Replace the literal with a translation key, e.g. accessibilityLabel={address || t('map.label')}.

Kody rule violation: Internationalize user-facing text with next-intl or next-i18next

Prompt for LLM

File src/components/maps/static-map.tsx:

Line 63:

Hardcoded user-visible string — the fallback accessibility label 'Map' is untranslated despite useTranslation() being available (line 28) and t() used elsewhere (line 55), breaking i18n consistency for screen-reader users. Replace the literal with a translation key, e.g. `accessibilityLabel={address || t('map.label')}`.

Talk to Kody by mentioning @kody

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

const URL_PATTERN = /^https?:\/\/.+/;
// Plain HTTP would send the OAuth2 password grant and bearer tokens in
// cleartext — only allow it in dev builds (local testing against localhost).
const URL_PATTERN = __DEV__ ? /^https?:\/\/.+/ : /^https:\/\/.+/;

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 critical

Plaintext HTTP exposure — the dev URL pattern permits arbitrary HTTP endpoints, violating Rule [61] and risking OAuth2 password-grant and bearer token exposure over HTTP since the regex does not restrict dev HTTP to localhost. Require HTTPS in all builds, or limit any dev-only HTTP exception to localhost/127.0.0.1 and enforce TLS 1.2+ on the server.

Kody rule violation: Enforce TLS 1.2+ and HSTS on all external endpoints

Prompt for LLM

File src/components/settings/server-url-bottom-sheet.tsx:

Line 30:

Plaintext HTTP exposure — the dev URL pattern permits arbitrary HTTP endpoints, violating Rule [61] and risking OAuth2 password-grant and bearer token exposure over HTTP since the regex does not restrict dev HTTP to localhost. Require HTTPS in all builds, or limit any dev-only HTTP exception to localhost/127.0.0.1 and enforce TLS 1.2+ on the server.

Talk to Kody by mentioning @kody

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

// genuine axios connectivity failure (no response received). Plain Errors are
// treated as server rejections and rethrown instead of queued.
const createNetworkError = (): Error => {
const error = new Error('Network Error') as any;

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 escape hatch — casting the Error to any to attach axios-specific properties defeats TypeScript's compiler checks, allowing typos on isAxiosError, code, and request to go undetected. Define a minimal interface (e.g., interface NetworkError extends Error { isAxiosError: true; code: string; request: unknown }) and cast to that, or use Object.assign(new Error('Network Error'), { isAxiosError: true, code: 'ERR_NETWORK', request: {} }).

Kody rule violation: Use safe type casting with as operator

Prompt for LLM

File src/components/status/__tests__/status-gps-integration-working.test.tsx:

Line 141:

Type safety escape hatch — casting the Error to `any` to attach axios-specific properties defeats TypeScript's compiler checks, allowing typos on isAxiosError, code, and request to go undetected. Define a minimal interface (e.g., `interface NetworkError extends Error { isAxiosError: true; code: string; request: unknown }`) and cast to that, or use `Object.assign(new Error('Network Error'), { isAxiosError: true, code: 'ERR_NETWORK', request: {} })`.

Talk to Kody by mentioning @kody

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

Comment thread src/hooks/use-saml-login.ts Outdated
return null;
}

const pendingRelayState = getItem<string>(SAML_RELAY_STATE_KEY);

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 from getItem() — the call omits await while sibling setItem (line 49) and removeItem (line 89) are awaited, leaving pendingRelayState holding a Promise rather than the stored nonce string, so the comparison relayState !== pendingRelayState on line 83 is always true and every legitimate callback is rejected; any underlying storage rejection is also unhandled. Change to const pendingRelayState = await getItem<string>(SAML_RELAY_STATE_KEY); so the value resolves and any rejection propagates to the caller's try/catch.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

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

Line 76:

Unhandled Promise from getItem() — the call omits await while sibling setItem (line 49) and removeItem (line 89) are awaited, leaving pendingRelayState holding a Promise rather than the stored nonce string, so the comparison `relayState !== pendingRelayState` on line 83 is always true and every legitimate callback is rejected; any underlying storage rejection is also unhandled. Change to `const pendingRelayState = await getItem<string>(SAML_RELAY_STATE_KEY);` so the value resolves and any rejection propagates to the caller's try/catch.

Talk to Kody by mentioning @kody

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

* Returns the SAMLResponse when valid, null otherwise. The nonce is consumed
* on use so a captured callback URL cannot be replayed.
*/
async function validateSamlCallback(url: string): Promise<string | null> {

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

Incomplete JSDoc — validateSamlCallback returns Promise<string | null> but its block (lines 61–65) lacks the formal @returns {Promise<string | null>} and @throws tags required by Rule [23]. Add proper JSDoc annotations documenting the resolve value (e.g., @returns {Promise<string | null>} The SAMLResponse string, or null if the callback is invalid.) and noting that storage read/remove failures can cause rejection.

Kody rule violation: Document async/Promise behavior and errors

Prompt for LLM

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

Line 66:

Incomplete JSDoc — validateSamlCallback returns Promise<string | null> but its block (lines 61–65) lacks the formal @returns {Promise<string | null>} and @throws tags required by Rule [23]. Add proper JSDoc annotations documenting the resolve value (e.g., `@returns {Promise<string | null>} The SAMLResponse string, or null if the callback is invalid.`) and noting that storage read/remove failures can cause rejection.

Talk to Kody by mentioning @kody

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

const cacheItem: CacheItem<T> = JSON.parse(cached);
let cacheItem: CacheItem<T>;
try {
cacheItem = JSON.parse(cached);

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 shape validation — JSON.parse is wrapped in try/catch for SyntaxError but the parsed object's structure is never verified before accessing cacheItem.timestamp and cacheItem.expiresIn on line 68, so valid JSON like {} yields undefined properties, makes isExpired compute NaN, and treats the entry as non-expired while serving malformed data. After parsing, validate structure: if (!cacheItem || typeof cacheItem.timestamp !== 'number' || typeof cacheItem.expiresIn !== 'number') { storage.delete(key); return null; }.

Kody rule violation: Always validate JSON parsing

Prompt for LLM

File src/lib/cache/cache-manager.ts:

Line 60:

Missing shape validation — JSON.parse is wrapped in try/catch for SyntaxError but the parsed object's structure is never verified before accessing cacheItem.timestamp and cacheItem.expiresIn on line 68, so valid JSON like `{}` yields undefined properties, makes isExpired compute NaN, and treats the entry as non-expired while serving malformed data. After parsing, validate structure: `if (!cacheItem || typeof cacheItem.timestamp !== 'number' || typeof cacheItem.expiresIn !== 'number') { storage.delete(key); return null; }`.

Talk to Kody by mentioning @kody

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

Comment thread src/lib/shared-ticker.ts Outdated
Comment on lines +16 to +18
} catch {
// A throwing listener must not kill the shared ticker for everyone else.
}

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

Silently swallowed exceptions — the catch block consumes every listener error with no logging or rethrow, violating Rule [30]'s requirement that exceptions be logged with context or handled explicitly, making misbehaving-subscriber debugging impossible. Log with structured context (e.g., logger.error('shared-ticker listener threw', { err })) or expose an error callback in the subscription API so subscribers can decide how to handle their own failures.

Kody rule violation: Avoid empty catch blocks

Prompt for LLM

File src/lib/shared-ticker.ts:

Line 16 to 18:

Silently swallowed exceptions — the catch block consumes every listener error with no logging or rethrow, violating Rule [30]'s requirement that exceptions be logged with context or handled explicitly, making misbehaving-subscriber debugging impossible. Log with structured context (e.g., logger.error('shared-ticker listener threw', { err })) or expose an error callback in the subscription API so subscribers can decide how to handle their own failures.

Talk to Kody by mentioning @kody

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

Comment thread src/services/signalr.service.ts Outdated
message: `Scheduling reconnection attempt ${currentAttempts}/${this.MAX_RECONNECT_ATTEMPTS} for hub: ${hubName} in ${delay}ms`,
});

setTimeout(async () => {

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

Leaked timer — setTimeout is invoked without storing its timer ID, leaving no deterministic cleanup path to cancel a pending reconnect timer as required by Rule [52], risking orphaned callbacks after teardown or explicit disconnect. Store the timer ID (e.g., this.reconnectTimers.set(hubName, setTimeout(...))) and clearTimeout it in the disconnect/teardown path.

Kody rule violation: Clear timers on teardown/unmount

Prompt for LLM

File src/services/signalr.service.ts:

Line 434:

Leaked timer — setTimeout is invoked without storing its timer ID, leaving no deterministic cleanup path to cancel a pending reconnect timer as required by Rule [52], risking orphaned callbacks after teardown or explicit disconnect. Store the timer ID (e.g., `this.reconnectTimers.set(hubName, setTimeout(...))`) and clearTimeout it in the disconnect/teardown path.

Talk to Kody by mentioning @kody

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

});
});

room.on(RoomEvent.Disconnected, () => {

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

Spurious RoomEvent.Disconnected handler invocation during intentional room switches — connectToRoom() awaits currentRoom.disconnect() (line 516) without first clearing isConnected, so the handler's if (!get().isConnected) return guard passes and runs the full unexpected-drop reset, firing callKeepService.endCall(), notifee.stopForegroundService(), and set({ currentRoom: null, isConnected: false }). Set isConnected: false before disconnecting the old room, matching the pattern already used in disconnectFromRoom (line 824).

// Disconnect from current room if connected
if (currentRoom) {
  logger.debug({ message: 'connectToRoom: disconnecting existing room' });
  // Mark disconnected FIRST so the RoomEvent.Disconnected handler treats
  // this as intentional (same pattern as disconnectFromRoom).
  set({ isConnected: false });
  await currentRoom.disconnect();
}
Prompt for LLM

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

Line 582:

Spurious RoomEvent.Disconnected handler invocation during intentional room switches — connectToRoom() awaits currentRoom.disconnect() (line 516) without first clearing isConnected, so the handler's `if (!get().isConnected) return` guard passes and runs the full unexpected-drop reset, firing callKeepService.endCall(), notifee.stopForegroundService(), and `set({ currentRoom: null, isConnected: false })`. Set `isConnected: false` before disconnecting the old room, matching the pattern already used in disconnectFromRoom (line 824).

Suggested Code:

      // Disconnect from current room if connected
      if (currentRoom) {
        logger.debug({ message: 'connectToRoom: disconnecting existing room' });
        // Mark disconnected FIRST so the RoomEvent.Disconnected handler treats
        // this as intentional (same pattern as disconnectFromRoom).
        set({ isConnected: false });
        await currentRoom.disconnect();
      }

Talk to Kody by mentioning @kody

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

Comment thread src/stores/auth/store.tsx
clearTimeout(existingTimeoutId);
}
// Retry after 30 seconds for transient errors
const retryTimeoutId = setTimeout(() => get().refreshAccessToken(), 30000);

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 — the literal 30000 inlined in setTimeout obscures intent and is inconsistent with the existing refreshDelayMs convention used for the proactive-refresh delay. Extract a named constant such as const REFRESH_RETRY_DELAY_MS = 30_000; (or reuse a shared time-constant utility) at the top of the module or in a constants file and use it in the setTimeout call.

Kody rule violation: Replace magic numbers with named constants

Prompt for LLM

File src/stores/auth/store.tsx:

Line 253:

Magic number — the literal 30000 inlined in setTimeout obscures intent and is inconsistent with the existing refreshDelayMs convention used for the proactive-refresh delay. Extract a named constant such as `const REFRESH_RETRY_DELAY_MS = 30_000;` (or reuse a shared time-constant utility) at the top of the module or in a constants file and use it in the setTimeout call.

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 26, 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.


beforeEach(() => {
jest.clearAllMocks();
mockDispatchStore.data.users[0].Name = 'John Doe';

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

Unguarded property access mockDispatchStore.data.users[0].Name risks a TypeError if data or users is null/undefined at runtime. Use optional chaining or assert the mock structure beforehand.

Kody rule violation: Add null checks before accessing properties

Prompt for LLM

File src/components/calls/__tests__/dispatch-selection-modal.test.tsx:

Line 175:

Unguarded property access `mockDispatchStore.data.users[0].Name` risks a TypeError if `data` or `users` is null/undefined at runtime. Use optional chaining or assert the mock structure beforehand.

Talk to Kody by mentioning @kody

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

});

it.each([undefined, null])('should not crash while filtering when a user name is %s', (missingName) => {
mockDispatchStore.data.users[0].Name = missingName as unknown as string;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Double cast missingName as unknown as string bypasses TypeScript type safety by forcing an explicitly undefined or null value to type string. Widen the mock type (e.g., Name?: string | null) and assign directly without the unsafe cast.

Kody rule violation: Use safe type casting with as operator

Prompt for LLM

File src/components/calls/__tests__/dispatch-selection-modal.test.tsx:

Line 232:

Double cast `missingName as unknown as string` bypasses TypeScript type safety by forcing an explicitly undefined or null value to type `string`. Widen the mock type (e.g., `Name?: string | null`) and assign directly without the unsafe cast.

Talk to Kody by mentioning @kody

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

return null;
}

const pendingRelayState = await getItem<string>(SAML_RELAY_STATE_KEY);

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: await getItem(...) lacks a surrounding try/catch or .catch, and a storage read can reject due to quota, serialization, or transport errors. Wrap the call in try/catch and handle the failure explicitly, or ensure the enclosing function already guards this call.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

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

Line 76:

Unhandled promise rejection risk: `await getItem(...)` lacks a surrounding try/catch or `.catch`, and a storage read can reject due to quota, serialization, or transport errors. Wrap the call in try/catch and handle the failure explicitly, or ensure the enclosing function already guards this call.

Talk to Kody by mentioning @kody

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

Comment thread src/lib/shared-ticker.ts
listeners.add(listener);

if (!interval) {
interval = setInterval(tick, 1000);

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 1000 obscures the tick interval's intent and is error-prone if the duration needs to change. Extract a named constant such as const TICK_INTERVAL_MS = 1000; at the top of the module.

Kody rule violation: Replace magic numbers with named constants

Prompt for LLM

File src/lib/shared-ticker.ts:

Line 36:

Magic number `1000` obscures the tick interval's intent and is error-prone if the duration needs to change. Extract a named constant such as `const TICK_INTERVAL_MS = 1000;` at the top of the module.

Talk to Kody by mentioning @kody

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

const service = SignalRService.getInstance();

await service.connectToHubWithEventingUrl(mockConfig);
const onCloseCallback = mockConnection.onclose.mock.calls[0][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

Accessing mock.calls[0][0] without verifying the array is non-empty throws a cryptic TypeError if onclose was never invoked. Add a length assertion or use optional chaining before indexing.

Kody rule violation: Check query results before accessing indices

Prompt for LLM

File src/services/__tests__/signalr.service.enhanced.test.ts:

Line 348:

Accessing `mock.calls[0][0]` without verifying the array is non-empty throws a cryptic TypeError if `onclose` was never invoked. Add a length assertion or use optional chaining before indexing.

Talk to Kody by mentioning @kody

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

@ucswift

ucswift commented Jul 26, 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 6f995da into master Jul 26, 2026
17 of 19 checks passed

@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: 17

Caution

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

⚠️ Outside diff range comments (4)
src/app/login/sso.tsx (1)

98-113: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle failed callback validation and login.

validateSamlCallback() performs storage I/O and ssoLogin() is awaited; either rejection becomes an unhandled promise rejection here. Wrap the handler body in try/catch, log the failure, and reset isSsoLoading before showing the error modal.

As per coding guidelines, “All async operations must have proper try/catch with logging.”

🤖 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/login/sso.tsx` around lines 98 - 113, Wrap the async URL handler
registered by Linking.addEventListener in try/catch so rejections from
validateSamlCallback and ssoLogin are handled. In the catch block, log the
failure, reset isSsoLoading, and show the error modal; preserve the existing
invalid-callback handling and successful ssoLogin flow.

Source: Coding guidelines

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

159-174: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Sentry never receives a real exception anymore — err instanceof Error can no longer be true.

sanitizeLogContext now converts real Error instances into plain {name, message, stack} objects (line 52-54) and axios-like errors into plain summary objects (line 37-46) before sanitized.error is checked at line 168. Since both paths strip the Error prototype, err instanceof Error is always false after this change, so Sentry.captureException is now dead code for every call site that logs context: { error } — every error report falls through to Sentry.captureMessage, losing stack traces and exception-based grouping in Sentry across the app.

🐛 Capture the raw error, not the sanitized copy
   public error(entry: LogEntry): void {
     this.log('error', entry);
     if (!isJest) {
+      const rawError = entry.context?.error;
       const sanitized = sanitizeLogContext({
         ...entry.context,
         ...(entry.operation ? { operation: entry.operation } : {}),
         ...(entry.trace_id ? { trace_id: entry.trace_id } : {}),
       });
-      const err = sanitized.error;
-      if (err instanceof Error) {
-        Sentry.captureException(err, { extra: { message: entry.message, ...sanitized } });
+      if (rawError instanceof Error) {
+        Sentry.captureException(rawError, { extra: { message: entry.message, ...sanitized } });
       } else {
         Sentry.captureMessage(entry.message, { level: 'error', extra: sanitized });
       }
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/logging/index.tsx` around lines 159 - 174, Update the error handling
in Logger.error so Sentry.captureException receives the raw Error from
entry.context before sanitizeLogContext converts it; use the sanitized context
only for Sentry metadata and retain captureMessage for non-Error values.

48-60: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Arrays bypass sanitization entirely.

The recursion guard excludes arrays (!Array.isArray(value)), so sanitizeValue never processes array elements — any array containing objects with sensitive fields (tokens, emails, credentials) is logged/reported completely unredacted.

🔒 Recurse into arrays too
   const sanitizeValue = (key: string, value: unknown, depth: number): unknown => {
     if (isSensitiveKey(key)) return '[REDACTED]';
     if (isAxiosErrorLike(value)) return summarizeAxiosError(value);
     if (value instanceof Error) {
       return { name: value.name, message: value.message, stack: value.stack };
     }
-    if (depth > 0 && value !== null && typeof value === 'object' && !Array.isArray(value)) {
+    if (depth > 0 && Array.isArray(value)) {
+      return value.map((item) => sanitizeValue(key, item, depth - 1));
+    }
+    if (depth > 0 && value !== null && typeof value === 'object') {
       return sanitizeObject(value as Record<string, unknown>, depth - 1);
     }
     if (typeof value === 'function' || typeof value === 'symbol') return String(value);
     return value;
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/logging/index.tsx` around lines 48 - 60, Update sanitizeValue to
recurse into arrays when depth > 0, sanitizing each element through the existing
sanitization logic so nested sensitive fields are redacted. Preserve the current
object recursion, depth limit, and primitive/function/symbol handling.
src/stores/signalr/signalr-store.ts (1)

281-303: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Restore the geolocation connected flag on reconnect.

connectGeolocationHub only sets isGeolocationHubConnected via onGeolocationConnect, but the SignalR service emits __hubReconnected:${name} after automatic reconnects. Subscribe to the hub-scoped reconnected lifecycle event and clear it on disconnect so the flag can recover without relying on a callback replay.

🤖 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 281 - 303, Update
connectGeolocationHub to subscribe to the hub-scoped
__hubReconnected:${Env.REALTIME_GEO_HUB_NAME} lifecycle event and set
isGeolocationHubConnected to true when it fires. Keep the existing
geoHubDisconnected listener setting the flag to false, and register the
reconnect listener alongside the other geolocation hub listeners.
🧹 Nitpick comments (13)
src/lib/utils.ts (1)

12-29: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Numeric epoch input silently produces null in parseApiUtcDate.

For typeof input === 'number', String(input) + appended 'Z' (e.g. "1690000000000Z") is not parseable by new Date(), so a valid epoch timestamp would incorrectly resolve to null/'Unknown' instead of the correct date. Current callers only pass strings, so this isn't hit today, but the function's unknown signature invites future numeric callers to hit this silently.

🛠️ Proposed fix: handle numeric input directly
   if (input instanceof Date) {
     return Number.isNaN(input.getTime()) ? null : input;
   }
+
+  if (typeof input === 'number') {
+    return Number.isNaN(input) || !Number.isFinite(input) ? null : new Date(input);
+  }

   let value = String(input).trim();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/utils.ts` around lines 12 - 29, Update parseApiUtcDate to handle
numeric input before converting values to strings: treat finite numbers as epoch
timestamps and construct the Date directly from the number, while returning null
for invalid numeric values. Preserve the existing Date and string parsing
behavior.
src/stores/signalr/signalr-store.ts (2)

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

Type lastUpdateTimestamps by event key.

Record<string, number> loses the UpdateHubEvent union you just introduced, so typos in consumer lookups (e.g. state.lastUpdateTimestamps[event] in src/hooks/use-map-signalr-updates.ts Line 24) compile silently. Partial<Record<UpdateHubEvent, number>> keeps the ?? 0 fallbacks working while catching bad keys.

♻️ Proposed change
   /** Per-event timestamps — consumers should subscribe to the events they care about. */
-  lastUpdateTimestamps: Record<string, number>;
+  lastUpdateTimestamps: Partial<Record<UpdateHubEvent, number>>;

The reconnect resync at Line 153 would then use the same type:

-            const timestamps: Record<string, number> = {};
+            const timestamps: Partial<Record<UpdateHubEvent, number>> = {};
🤖 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 49 - 50, Update the
lastUpdateTimestamps property in the signalr store state to use
Partial<Record<UpdateHubEvent, number>> instead of Record<string, number>,
preserving undefined values for existing ?? 0 fallbacks and propagating the same
event-keyed type through the reconnect resync logic.

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

lastGeolocationMessage / lastGeolocationTimestamp are now dead state.

With the no-op listeners at Lines 298-299 nothing ever writes these fields; they stay null/0 and are only cleared again in disconnectGeolocationHub. Keeping them in SignalRState advertises a contract the store no longer fulfills. Either drop them from the interface and the two set calls, or mark them @deprecated like the update-hub aggregates at Lines 45-48.

Also applies to: 293-299

🤖 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 54 - 56, Remove
lastGeolocationMessage and lastGeolocationTimestamp from SignalRState, along
with their corresponding set calls in the geolocation listener/disconnect logic
around disconnectGeolocationHub; keep isGeolocationHubConnected and the no-op
listeners unchanged.
src/hooks/use-saml-login.ts (1)

44-59: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Pending nonce has no expiry or abandonment cleanup.

If the user backs out of the browser, saml_pending_relay_state stays in storage forever and is accepted by a later callback. Not exploitable (an attacker still needs the exact UUID), but storing { nonce, createdAt } and rejecting entries older than a few minutes in validateSamlCallback tightens the window.

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

In `@src/hooks/use-saml-login.ts` around lines 44 - 59, Update the SAML
relay-state flow around startSamlLogin and validateSamlCallback to persist the
nonce together with a creation timestamp, reject and clear stored entries older
than a few minutes during callback validation, and remove the pending entry when
the browser flow is abandoned or otherwise cannot complete.
src/hooks/use-status-signalr-updates.ts (1)

35-56: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Trailing-only debounce can starve under a sustained event stream.

The timer resets on every new message, so status events arriving continuously less than DEBOUNCE_DELAY apart would never trigger a refresh. Filtering on message.UnitId === activeUnitId makes that unlikely in practice, but a max-wait (fire if the first pending update is older than, say, 5s) would guarantee progress.

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

In `@src/hooks/use-status-signalr-updates.ts` around lines 35 - 56, Update the
debounce logic in the SignalR status-update handler so continuous events cannot
indefinitely postpone refreshActiveUnitStatus. Preserve the trailing debounce
while adding a maximum wait from the first pending update (for example, 5
seconds), triggering the refresh when that limit is reached and resetting the
max-wait state after execution.
src/stores/signalr/__tests__/signalr-store.test.ts (1)

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

Flushing the fire-and-forget reconnect handler via setTimeout(0) is fragile.

The __hubReconnected handler is void (async () => …)() in the store; a single macrotask tick happens to flush it today, but adding another await inside the handler would silently break this test. Consider having the handler expose its promise, or drain with a small loop of microtask flushes.

🤖 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/__tests__/signalr-store.test.ts` around lines 332 - 335,
Replace the single setTimeout(0) wait in the __hubReconnected:eventingHub test
with a reliable drain of the fire-and-forget handler’s asynchronous work,
preferably by exposing and awaiting its promise; otherwise use a bounded loop of
microtask flushes. Keep the existing reconnect assertions and handler behavior
unchanged.
src/hooks/__tests__/use-map-signalr-updates.test.ts (1)

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

Add a negative case for non-map-relevant events.

The whole point of the new selector is that events like weatherAlertReceived must not trigger a marker refetch, but every test only seeds callsUpdated. A test that sets lastUpdateTimestamps: { weatherAlertReceived: t } and asserts getMapDataAndMarkers is never called would lock in the filtering behavior.

♻️ Suggested helper + test
 const stateWithUpdate = (timestamp: number) => ({
   lastUpdateTimestamps: timestamp > 0 ? { callsUpdated: timestamp } : {},
 });
+const mockStoreWithEvent = (event: string, timestamp: number) => {
+  const state = { lastUpdateTimestamps: { [event]: timestamp } };
+  mockUseSignalRStore.mockImplementation((selector: any) => (typeof selector === 'function' ? selector(state) : state));
+};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/__tests__/use-map-signalr-updates.test.ts` around lines 20 - 29,
Add a negative test for the selector used by useMapSignalRUpdates that seeds
lastUpdateTimestamps with weatherAlertReceived instead of callsUpdated, then
verifies getMapDataAndMarkers is never called. Extend or reuse
stateWithUpdate/mockStoreWithTimestamp only as needed to represent this
non-map-relevant event while preserving existing map-update tests.
src/stores/app/__tests__/livekit-store-room-switch.test.ts (1)

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

Place native-module mocks above the imports.

jest.mock calls are hoisted so behavior is correct, but the repo convention is mocks first.

As per coding guidelines: "Mock native modules at the top of test files before 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/app/__tests__/livekit-store-room-switch.test.ts` around lines 1 -
32, Move the native-module jest.mock declarations for `@livekit/react-native`,
`@livekit/react-native-webrtc`, and `@notifee/react-native` above all imports in the
test file, keeping their existing mock implementations unchanged.

Source: Coding guidelines

src/services/offline-event-manager.service.ts (1)

233-241: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Rename MAX_CONCURRENT_EVENTS — it is now a per-batch cap, and it bounds drain rate.

With sequential processing the constant no longer describes concurrency, and 3 events per 10s tick means a large offline backlog drains slowly (100 queued events ≈ 5.5 minutes of connectivity). Consider MAX_EVENTS_PER_BATCH and a larger batch size, since ordering is now guaranteed by the awaited loop rather than by the cap.

♻️ Proposed rename
-    const eventsToProcess = [...pendingEvents].sort((a, b) => a.createdAt - b.createdAt).slice(0, this.MAX_CONCURRENT_EVENTS);
+    const eventsToProcess = [...pendingEvents].sort((a, b) => a.createdAt - b.createdAt).slice(0, this.MAX_EVENTS_PER_BATCH);
🤖 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/offline-event-manager.service.ts` around lines 233 - 241, Rename
MAX_CONCURRENT_EVENTS to a per-batch limit such as MAX_EVENTS_PER_BATCH
throughout OfflineEventManagerService, including the slice used to build
eventsToProcess. Increase the batch limit so sequential processing drains
backlogs faster, while preserving creation-order sorting and the awaited
processEvent loop.
src/services/__tests__/signalr.service.enhanced.test.ts (1)

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

Tests assert on private internals via as any.

reconnectTimers / hubConfigs are private; these assertions break on any internal rename and bypass the "no any" rule. Consider asserting observable behavior instead (e.g. no connectToHubWithEventingUrl/refreshAccessToken call after advancing timers, which lines 329/361 already cover), or expose a small test-only accessor.

As per coding guidelines: "Never use any type; use precise types and interfaces with TypeScript strict mode enabled".

Also applies to: 344-362

🤖 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/__tests__/signalr.service.enhanced.test.ts` at line 317, Replace
the `(service as any).reconnectTimers` and `(service as any).hubConfigs`
assertions in the reconnect timer tests with observable behavior, reusing the
existing `connectToHubWithEventingUrl` and `refreshAccessToken` expectations
after advancing timers. Remove all `any` casts while preserving coverage of the
timer and configuration behavior.

Source: Coding guidelines

src/services/__tests__/location.test.ts (1)

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

Tests mutate shared fixtures without teardown, leaking state into later tests in the same file. Both cases overwrite module-level mock state inside a test body and rely on the test running to completion (or not at all) to restore it; a failing assertion leaves the fixture corrupted for every subsequent test.

  • src/services/__tests__/location.test.ts#L631-L645: move the mockCoreStoreState.activeUnitId = 'unit-123' restore out of the test body and into an afterEach (or re-assign it in the shared beforeEach).
  • src/services/__tests__/signalr.service.test.ts#L946-L984: reset mockRefreshAccessToken's implementation and mockGetState's return value in beforeEach/afterEach so the accessToken: null state does not persist past this test.
🤖 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/__tests__/location.test.ts` around lines 631 - 645, The tests
leak shared mock state when assertions fail. In
src/services/__tests__/location.test.ts lines 631-645, move restoration of
mockCoreStoreState.activeUnitId to the shared beforeEach or afterEach, removing
reliance on the test body. In src/services/__tests__/signalr.service.test.ts
lines 946-984, reset mockRefreshAccessToken’s implementation and mockGetState’s
return value in beforeEach or afterEach so the accessToken: null setup is
isolated to that test.
src/stores/check-in-timers/store.ts (1)

114-133: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider triggering an immediate refetch when returning to foreground.

Backgrounding the poll is a good battery fix, but after resuming the app can show up to intervalMs (30s) of stale check-in timer data until the next scheduled tick. Subscribing to AppState changes and calling fetchTimerStatuses(callId) immediately on transition to 'active' (mirroring the pattern in location.ts) would tighten that window for this safety-relevant countdown.

🤖 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/check-in-timers/store.ts` around lines 114 - 133, Update
startPolling to subscribe to AppState changes and immediately call
fetchTimerStatuses(callId) when transitioning to the active state, matching the
existing location.ts pattern. Store or clean up the listener alongside the
polling interval so repeated startPolling calls do not create duplicate
foreground handlers.
src/stores/status/store.ts (1)

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

Server-rejected status saves are now logged (and reported to Sentry) twice.

The inner catch logs the rejection and rethrows when !isNetworkError(error); that rethrow is caught again by the outer try/catch, which logs the same error a second time and re-sets the identical error state before rethrowing again. Since logger.error() auto-reports to Sentry, every server-rejected status save now produces two Sentry events for one failure. Consider letting the inner handler set state without throwing (return instead), or having the outer catch skip re-logging when the error already has its state set, so each real failure is reported once.

As per coding guidelines, **/*.{ts,tsx}: "logger.error() automatically reports to Sentry; sensitive keys are redacted from context" — duplicate logger.error() calls for one failure double the Sentry noise for this 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/stores/status/store.ts` around lines 244 - 321, Prevent server-rejected
status saves from being logged twice: in the non-network-error branch of the
inner handler, preserve the existing logger.error call and error state update,
then return without rethrowing into the outer catch. Keep network failures on
the existing offline-queue path and preserve the outer catch for errors not
already handled.

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/_layout.tsx`:
- Around line 4-6: Move the side-effect import for the app reset registration to
the side-effect import group in the module’s import section, keeping it before
all value imports. Preserve the existing registration behavior and ordering
required by simple-import-sort.

In `@src/app/call/__tests__/`[id].test.tsx:
- Around line 1362-1374: Update the test render in
src/app/call/__tests__/[id].test.tsx at lines 1362-1374 to destructure unmount
from render and call it after the assertions. Apply the same cleanup in
src/components/calls/__tests__/dispatch-selection-modal.test.tsx at lines
235-237 by destructuring unmount and invoking it after assertions.
- Around line 285-290: Replace the any type in the mocked FullScreenMap
component with a small props interface containing the required isOpen field and
an appropriate type for forwarded props, keeping the existing mock rendering
behavior unchanged.

In `@src/app/routes/stop/`[id].tsx:
- Around line 218-222: Normalize PlannedArrival and PlannedDeparture to an
explicit UTC timestamp contract before passing them to safeFormatDate, and
update the corresponding raw rendering in stop-card.tsx and new Date() usage in
routes/start.tsx to use the same parsing. Preserve the existing display behavior
while ensuring timezone-less values are not interpreted as local time.

In `@src/components/check-in-timers/__tests__/check-in-timer-card.test.tsx`:
- Around line 4-8: Reorder the imports in the check-in timer card test so the
relative value import of CheckInTimerCard appears before the type-only
CheckInTimerStatusResultData import, preserving the existing side-effect,
external, and alias import groups.

In `@src/components/maps/static-map.tsx`:
- Around line 31-34: Update the coordinate validation in the imageUrl useMemo
callback to accept valid zero-valued latitude and longitude coordinates. Replace
the truthiness check with finite-number validation, returning null only when
either coordinate is not finite.

In `@src/components/status/__tests__/status-bottom-sheet.test.tsx`:
- Around line 200-203: Replace the any-typed mockStore in the location-store
Jest mock with a minimal location-state interface and a generic selector
function type, and type getState() to return that state. Apply the same precise
typing to the related mock setup around the additional referenced section,
preserving selector and getState contract checks without using any.

In `@src/components/status/__tests__/status-gps-integration.test.tsx`:
- Around line 46-55: Replace the any-cast network-error fixtures in
createNetworkError and its counterpart in
src/components/status/__tests__/status-gps-integration.test.tsx (lines 46-55)
and src/components/status/__tests__/status-gps-integration-working.test.tsx
(lines 137-146) with a minimal precisely typed Axios-like error interface or
typed Object.assign, preserving the isAxiosError, code, and request properties.

In `@src/hooks/__tests__/use-saml-login.test.ts`:
- Around line 176-192: Update the storage mocks in the handleDeepLink tests to
match the synchronous getItem contract: replace the rejected-promise mock with a
synchronous mockImplementation that throws for the read-failure case, and
replace the resolved-value mock at the other referenced test with the
appropriate synchronous mockReturnValue. Keep the existing assertions and hook
behavior unchanged.

In `@src/lib/logging/index.tsx`:
- Around line 126-137: Update the private log method to sanitize the merged
logging context before passing it to this.logger[level]. Reuse the existing
sanitizeLogContext helper, ensuring globalContext, context, operation, and
trace_id are all covered while preserving the current severity filtering and
timestamp behavior.

In `@src/services/push-notification.ts`:
- Around line 477-502: Extend unregisterFromPushNotifications to invalidate the
device’s push token server-side during logout, adding or calling the backend
unregister-device operation before or alongside the existing local cleanup.
Ensure the request identifies the current device/token and completes or safely
handles failure without skipping local state clearing, then update the method’s
comment to reflect the implemented behavior.

In `@src/services/signalr.service.ts`:
- Around line 453-535: Update the reconnect callback around reconnectTimers and
connectToHubWithEventingUrl to invalidate in-flight reconnects when
disconnectFromHub, disconnectAll, or resetInstance tears down a hub. Add a
per-hub generation/epoch (or equivalent disconnected marker), capture it when
scheduling the callback, and verify it after refreshAccessToken and before
reconnecting or applying success state; abort when the generation changed or hub
configuration was removed. Ensure teardown increments or marks the generation so
the callback cannot recreate configuration or reopen a connection.
- Around line 445-446: Update the reconnect delay calculation in the SignalR
service to use true exponential backoff: base RECONNECT_INTERVAL multiplied by 2
to the power of currentAttempts minus one, still capped at 60000 milliseconds.
Keep the existing attempt sequence and cap behavior consistent with the comment.

In `@src/stores/app/__tests__/livekit-store-room-switch.test.ts`:
- Around line 86-89: Update the test setup in livekit-store-room-switch.test.ts
so the stopForegroundService assertion exercises the Android-specific branch by
setting Platform.OS to "android" for that case. Preserve the existing
intentional-switch guard coverage and ensure the assertion would fail if
foreground-service teardown were incorrectly triggered.

In `@src/stores/app/livekit-store.ts`:
- Around line 585-592: Update the RoomEvent.Disconnected handler to verify that
the event’s room is still the store’s active/current room before resetting call
state. Keep the existing isConnected guard, but add a room-identity check using
the room instance captured by this handler and the store’s current room
reference, returning early for stale rooms so currentRoom, CallKeep, and
foreground-service state remain intact.

In `@src/stores/auth/store.tsx`:
- Around line 339-350: Update the auth store rehydration flow around partialize
and refreshAccessToken so a persisted, non-expired accessToken immediately
restores status to signedIn, using refreshTokenExpiresOn for expiry validation.
Set status to loading only when rehydration has a refresh token without a usable
access token, while preserving the existing refresh retry behavior for transient
failures.

In `@src/stores/signalr/__tests__/zz-dbg.test.ts`:
- Around line 1-48: Delete the scratch test artifact zz-dbg.test.ts entirely;
its console logging, duplicated mocks, and assertion-free department-join
scenario are already covered by signalr-store.test.ts.

---

Outside diff comments:
In `@src/app/login/sso.tsx`:
- Around line 98-113: Wrap the async URL handler registered by
Linking.addEventListener in try/catch so rejections from validateSamlCallback
and ssoLogin are handled. In the catch block, log the failure, reset
isSsoLoading, and show the error modal; preserve the existing invalid-callback
handling and successful ssoLogin flow.

In `@src/lib/logging/index.tsx`:
- Around line 159-174: Update the error handling in Logger.error so
Sentry.captureException receives the raw Error from entry.context before
sanitizeLogContext converts it; use the sanitized context only for Sentry
metadata and retain captureMessage for non-Error values.
- Around line 48-60: Update sanitizeValue to recurse into arrays when depth > 0,
sanitizing each element through the existing sanitization logic so nested
sensitive fields are redacted. Preserve the current object recursion, depth
limit, and primitive/function/symbol handling.

In `@src/stores/signalr/signalr-store.ts`:
- Around line 281-303: Update connectGeolocationHub to subscribe to the
hub-scoped __hubReconnected:${Env.REALTIME_GEO_HUB_NAME} lifecycle event and set
isGeolocationHubConnected to true when it fires. Keep the existing
geoHubDisconnected listener setting the flag to false, and register the
reconnect listener alongside the other geolocation hub listeners.

---

Nitpick comments:
In `@src/hooks/__tests__/use-map-signalr-updates.test.ts`:
- Around line 20-29: Add a negative test for the selector used by
useMapSignalRUpdates that seeds lastUpdateTimestamps with weatherAlertReceived
instead of callsUpdated, then verifies getMapDataAndMarkers is never called.
Extend or reuse stateWithUpdate/mockStoreWithTimestamp only as needed to
represent this non-map-relevant event while preserving existing map-update
tests.

In `@src/hooks/use-saml-login.ts`:
- Around line 44-59: Update the SAML relay-state flow around startSamlLogin and
validateSamlCallback to persist the nonce together with a creation timestamp,
reject and clear stored entries older than a few minutes during callback
validation, and remove the pending entry when the browser flow is abandoned or
otherwise cannot complete.

In `@src/hooks/use-status-signalr-updates.ts`:
- Around line 35-56: Update the debounce logic in the SignalR status-update
handler so continuous events cannot indefinitely postpone
refreshActiveUnitStatus. Preserve the trailing debounce while adding a maximum
wait from the first pending update (for example, 5 seconds), triggering the
refresh when that limit is reached and resetting the max-wait state after
execution.

In `@src/lib/utils.ts`:
- Around line 12-29: Update parseApiUtcDate to handle numeric input before
converting values to strings: treat finite numbers as epoch timestamps and
construct the Date directly from the number, while returning null for invalid
numeric values. Preserve the existing Date and string parsing behavior.

In `@src/services/__tests__/location.test.ts`:
- Around line 631-645: The tests leak shared mock state when assertions fail. In
src/services/__tests__/location.test.ts lines 631-645, move restoration of
mockCoreStoreState.activeUnitId to the shared beforeEach or afterEach, removing
reliance on the test body. In src/services/__tests__/signalr.service.test.ts
lines 946-984, reset mockRefreshAccessToken’s implementation and mockGetState’s
return value in beforeEach or afterEach so the accessToken: null setup is
isolated to that test.

In `@src/services/__tests__/signalr.service.enhanced.test.ts`:
- Line 317: Replace the `(service as any).reconnectTimers` and `(service as
any).hubConfigs` assertions in the reconnect timer tests with observable
behavior, reusing the existing `connectToHubWithEventingUrl` and
`refreshAccessToken` expectations after advancing timers. Remove all `any` casts
while preserving coverage of the timer and configuration behavior.

In `@src/services/offline-event-manager.service.ts`:
- Around line 233-241: Rename MAX_CONCURRENT_EVENTS to a per-batch limit such as
MAX_EVENTS_PER_BATCH throughout OfflineEventManagerService, including the slice
used to build eventsToProcess. Increase the batch limit so sequential processing
drains backlogs faster, while preserving creation-order sorting and the awaited
processEvent loop.

In `@src/stores/app/__tests__/livekit-store-room-switch.test.ts`:
- Around line 1-32: Move the native-module jest.mock declarations for
`@livekit/react-native`, `@livekit/react-native-webrtc`, and `@notifee/react-native`
above all imports in the test file, keeping their existing mock implementations
unchanged.

In `@src/stores/check-in-timers/store.ts`:
- Around line 114-133: Update startPolling to subscribe to AppState changes and
immediately call fetchTimerStatuses(callId) when transitioning to the active
state, matching the existing location.ts pattern. Store or clean up the listener
alongside the polling interval so repeated startPolling calls do not create
duplicate foreground handlers.

In `@src/stores/signalr/__tests__/signalr-store.test.ts`:
- Around line 332-335: Replace the single setTimeout(0) wait in the
__hubReconnected:eventingHub test with a reliable drain of the fire-and-forget
handler’s asynchronous work, preferably by exposing and awaiting its promise;
otherwise use a bounded loop of microtask flushes. Keep the existing reconnect
assertions and handler behavior unchanged.

In `@src/stores/signalr/signalr-store.ts`:
- Around line 49-50: Update the lastUpdateTimestamps property in the signalr
store state to use Partial<Record<UpdateHubEvent, number>> instead of
Record<string, number>, preserving undefined values for existing ?? 0 fallbacks
and propagating the same event-keyed type through the reconnect resync logic.
- Around line 54-56: Remove lastGeolocationMessage and lastGeolocationTimestamp
from SignalRState, along with their corresponding set calls in the geolocation
listener/disconnect logic around disconnectGeolocationHub; keep
isGeolocationHubConnected and the no-op listeners unchanged.

In `@src/stores/status/store.ts`:
- Around line 244-321: Prevent server-rejected status saves from being logged
twice: in the non-network-error branch of the inner handler, preserve the
existing logger.error call and error state update, then return without
rethrowing into the outer catch. Keep network failures on the existing
offline-queue path and preserve the outer catch for errors not already handled.
🪄 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: 6b03adec-cb8f-4ba0-a539-2c2502423d7a

📥 Commits

Reviewing files that changed from the base of the PR and between acf8280 and 9362012.

📒 Files selected for processing (88)
  • app.config.ts
  • electron/main.js
  • src/api/common/__tests__/client.test.ts
  • src/api/common/client.tsx
  • src/api/notes/notes.ts
  • src/app/(app)/__tests__/index.test.tsx
  • src/app/(app)/__tests__/protocols.test.tsx
  • src/app/(app)/_layout.tsx
  • src/app/(app)/calls.tsx
  • src/app/(app)/contacts.tsx
  • src/app/(app)/index.tsx
  • src/app/(app)/notes.tsx
  • src/app/(app)/protocols.tsx
  • src/app/(app)/settings.tsx
  • src/app/(app)/weather-alerts.tsx
  • src/app/_layout.tsx
  • src/app/call/[id].tsx
  • src/app/call/[id]/edit.tsx
  • src/app/call/__tests__/[id].test.tsx
  • src/app/call/new/index.tsx
  • src/app/login/sso.tsx
  • src/app/routes/active.tsx
  • src/app/routes/stop/[id].tsx
  • src/components/call-video-feeds/video-player-modal.tsx
  • src/components/calls/__tests__/dispatch-selection-modal.test.tsx
  • src/components/calls/call-card.tsx
  • src/components/calls/call-images-modal.tsx
  • src/components/calls/dispatch-selection-modal.tsx
  • src/components/check-in-timers/__tests__/check-in-timer-card.test.tsx
  • src/components/check-in-timers/check-in-timer-card.tsx
  • src/components/contacts/contact-card.tsx
  • src/components/maps/__tests__/full-screen-map.test.tsx
  • src/components/maps/full-screen-map.tsx
  • src/components/maps/static-map.tsx
  • src/components/notes/note-card.tsx
  • src/components/settings/server-url-bottom-sheet.tsx
  • src/components/status/__tests__/status-bottom-sheet.test.tsx
  • src/components/status/__tests__/status-gps-integration-working.test.tsx
  • src/components/status/__tests__/status-gps-integration.test.tsx
  • src/components/status/status-bottom-sheet.tsx
  • src/components/ui/html-renderer/index.web.tsx
  • src/components/ui/utils.tsx
  • src/features/livekit-call/store/useLiveKitCallStore.ts
  • src/hooks/__tests__/use-map-signalr-updates.test.ts
  • src/hooks/__tests__/use-saml-login.test.ts
  • src/hooks/__tests__/use-status-signalr-updates.test.tsx
  • src/hooks/use-map-signalr-updates.ts
  • src/hooks/use-saml-login.ts
  • src/hooks/use-status-signalr-updates.ts
  • src/lib/__tests__/protocol-utils.test.ts
  • src/lib/__tests__/shared-ticker.test.ts
  • src/lib/auth/api.tsx
  • src/lib/auth/refresh-lock.ts
  • src/lib/auth/session-cleanup.ts
  • src/lib/auth/types.tsx
  • src/lib/cache/__tests__/cache-manager.test.ts
  • src/lib/cache/cache-manager.ts
  • src/lib/logging/index.tsx
  • src/lib/logging/types.tsx
  • src/lib/protocol-utils.ts
  • src/lib/shared-ticker.ts
  • src/lib/storage/index.tsx
  • src/lib/utils.ts
  • src/services/__tests__/app-reset.service.test.ts
  • src/services/__tests__/location-foreground-permissions.test.ts
  • src/services/__tests__/location.test.ts
  • src/services/__tests__/signalr.service.enhanced.test.ts
  • src/services/__tests__/signalr.service.test.ts
  • src/services/app-reset.service.ts
  • src/services/check-in-notification.service.ts
  • src/services/location.ts
  • src/services/offline-event-manager.service.ts
  • src/services/push-notification.ts
  • src/services/signalr.service.ts
  • src/stores/app/__tests__/livekit-store-room-switch.test.ts
  • src/stores/app/core-store.ts
  • src/stores/app/livekit-store.ts
  • src/stores/app/server-url-store.ts
  • src/stores/auth/store.tsx
  • src/stores/calls/detail-store.ts
  • src/stores/calls/store.ts
  • src/stores/check-in-timers/store.ts
  • src/stores/offline-queue/store.ts
  • src/stores/signalr/__tests__/signalr-store.test.ts
  • src/stores/signalr/__tests__/zz-dbg.test.ts
  • src/stores/signalr/signalr-store.ts
  • src/stores/status/__tests__/store.test.ts
  • src/stores/status/store.ts
💤 Files with no reviewable changes (3)
  • src/app/call/new/index.tsx
  • src/components/ui/utils.tsx
  • src/app/call/[id]/edit.tsx

Comment thread src/app/_layout.tsx
Comment on lines +4 to +6
// Side-effect import: registers the full app-data wipe as the session-cleanup
// handler for every logout path (manual, forced 401, refresh rejection).
import '@/services/app-reset.service';

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

Place the side-effect import in the side-effect import group.

Keep this registration import before value imports so the required import ordering remains lint-compliant. As per coding guidelines, ESLint enforces simple-import-sort with grouping order: side-effect imports, external packages, internal aliases, relative imports, type 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/app/_layout.tsx` around lines 4 - 6, Move the side-effect import for the
app reset registration to the side-effect import group in the module’s import
section, keeping it before all value imports. Preserve the existing registration
behavior and ordering required by simple-import-sort.

Source: Coding guidelines

Comment on lines +285 to +290
jest.mock('@/components/maps/full-screen-map', () => ({
FullScreenMap: ({ isOpen, ...props }: any) => {
const React = require('react');
return isOpen ? React.createElement('full-screen-map', { ...props, testID: 'full-screen-call-map' }) : null;
},
}));

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

🧩 Analysis chain

🏁 Script executed:

rg -n --type tsx '\bany\b' 'src/app/call/__tests__/[id].test.tsx'

Repository: Resgrid/Unit

Length of output: 178


🏁 Script executed:

#!/bin/bash
set -euo to

# Locate and inspect the relevant test file and occurrences of mock any.
if [ -f 'src/app/call/__tests__/[id].test.tsx' ]; then
  echo "== file exists =="
  wc -l 'src/app/call/__tests__/[id].test.tsx'
  echo
  echo "== relevant lines =="
  sed -n '260,305p' 'src/app/call/__tests__/[id].test.tsx' | nl -ba -v260
  echo
  echo "== any occurrences =="
  grep -n '\bany\b' 'src/app/call/__tests__/[id].test.tsx' || true
else
  echo "src/app/call/__tests__/[id].test.tsx not found"
  echo "candidate files:"
  fd -a '\[id\]\.test\.tsx' . || true
fi

Repository: Resgrid/Unit

Length of output: 193


🏁 Script executed:

#!/bin/bash
set -eu # without the invalid `o` from `to`

# Locate and inspect the relevant test file and occurrences of explicit any.
if [ -f 'src/app/call/__tests__/[id].test.tsx' ]; then
  echo "== file exists =="
  wc -l 'src/app/call/__tests__/[id].test.tsx'
  echo
  echo "== relevant lines =="
  sed -n '260,305p' 'src/app/call/__tests__/[id].test.tsx' | nl -ba -v260
  echo
  echo "== explicit any occurrences =="
  grep -n '\bany\b' 'src/app/call/__tests__/[id].test.tsx' || true
else
  echo "src/app/call/__tests__/[id].test.tsx not found"
  echo "candidate files:"
  fd -a '\[id\]\.test\.tsx' . || true
fi

Repository: Resgrid/Unit

Length of output: 273


🏁 Script executed:

#!/bin/bash
set -eu

if [ -f 'src/app/call/__tests__/[id].test.tsx' ]; then
  wc -l 'src/app/call/__tests__/[id].test.tsx'
  cat -n 'src/app/call/__tests__/[id].test.tsx' | sed -n '260,305p'
  echo "-- explicit any occurrences --"
  grep -n '\bany\b' 'src/app/call/__tests__/[id].test.tsx' || true
else
  echo "src/app/call/__tests__/[id].test.tsx not found"
fi

Repository: Resgrid/Unit

Length of output: 5227


Replace the any mock props type.

Use a small FullScreenMap props interface instead of any so the test stays aligned with the component contract.

🤖 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/__tests__/`[id].test.tsx around lines 285 - 290, Replace the any
type in the mocked FullScreenMap component with a small props interface
containing the required isOpen field and an appropriate type for forwarded
props, keeping the existing mock rendering behavior unchanged.

Source: Coding guidelines

Comment on lines +1362 to +1374
const { getByTestId, queryByTestId } = render(<CallDetail />);

expect(queryByTestId('full-screen-call-map')).toBeNull();
fireEvent.press(await waitFor(() => getByTestId('call-detail-static-map')));

const fullScreenMap = getByTestId('full-screen-call-map');
expect(fullScreenMap.props).toMatchObject({
latitude: 40.7128,
longitude: -74.006,
title: 'Test Call',
address: '123 Main St',
});
});

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

Unmount rendered trees in the added tests.

  • src/app/call/__tests__/[id].test.tsx#L1362-L1374: destructure unmount from render() and call it after assertions.
  • src/components/calls/__tests__/dispatch-selection-modal.test.tsx#L235-L237: destructure unmount from render() and call it after assertions.

As per coding guidelines, “Always call unmount() in tests to clean up.”

📍 Affects 2 files
  • src/app/call/__tests__/[id].test.tsx#L1362-L1374 (this comment)
  • src/components/calls/__tests__/dispatch-selection-modal.test.tsx#L235-L237
🤖 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/__tests__/`[id].test.tsx around lines 1362 - 1374, Update the
test render in src/app/call/__tests__/[id].test.tsx at lines 1362-1374 to
destructure unmount from render and call it after the assertions. Apply the same
cleanup in src/components/calls/__tests__/dispatch-selection-modal.test.tsx at
lines 235-237 by destructuring unmount and invoking it after assertions.

Source: Coding guidelines

Comment on lines +218 to +222
<Text className="text-sm font-medium">{safeFormatDate(stop.PlannedArrival, 'MMM d, h:mm a')}</Text>
</VStack>
<VStack>
<Text className="text-xs text-typography-500">{t('routes.planned_departure')}</Text>
<Text className="text-sm font-medium">{stop.PlannedDeparture ? format(new Date(stop.PlannedDeparture), 'MMM d, h:mm a') : '--'}</Text>
<Text className="text-sm font-medium">{safeFormatDate(stop.PlannedDeparture, 'MMM d, h:mm a')}</Text>

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:

fd -i 'routeInstanceStopResultData.ts' src/models
rg -n -C2 '\bPlanned(Arrival|Departure)\b' src

Repository: Resgrid/Unit

Length of output: 3085


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Candidate date utility files"
fd -i 'date|time|format|date.*util' src | sed -n '1,120p'

echo
echo "## safeFormatDate and related date utility definitions"
rg -n -C3 'function safeFormatDate|const safeFormatDate|parseApiUtcDate|formatDateTime|format.*Date|safeDate' src

echo
echo "## Planned timestamp formatting usage"
rg -n -C3 'PlannedArrival|PlannedDeparture|parseApiUtcDate' src/app src/components src/lib src/services src/models

echo
echo "## stop detail relevant section"
sed -n '180,235p' src/app/routes/stop/[id].tsx

echo
echo "## stop-card relevant section"
sed -n '55,75p' src/components/routes/stop-card.tsx

Repository: Resgrid/Unit

Length of output: 40740


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## src/lib/utils.ts parseApiUtcDate implementation"
sed -n '1,55p' src/lib/utils.ts

echo
echo "## src/lib/utils.ts formatDateForDisplay implementation"
sed -n '155,255p' src/lib/utils.ts

echo
echo "## src/lib/utils.ts parseWeatherAlertDate implementation"
sed -n '395,430p' src/lib/utils.ts

echo
echo "## src/lib/utils.ts safeFormatDate implementation"
sed -n '305,355p' src/lib/utils.ts

echo
echo "## src/app/call/[id].tsx imports around safeFormatDate"
sed -n '25,35p' src/app/call/[id].tsx

echo
echo "## src/app/routes/start.tsx imports and formatDateTime implementation"
sed -n '1,35p' src/app/routes/start.tsx
sed -n '21,38p' src/app/routes/start.tsx

echo
echo "## Standalone JS behavior for timezone-less ISO strings"
node - <<'JS'
const input = '2026-07-01T12:30:00';
console.log({
  native_timestamp: new Date(input).getTime(),
  naive_treated_as_localish_date: new Date(input).toString(),
});
console.log('native local ISO for naive input:', new Date input.toISOString());
console.log('fixed UTC parse for naive input:', new Date(input + 'Z').toISOString());
JS

Repository: Resgrid/Unit

Length of output: 11698


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const naive = '2026-07-01T12:30:00';
const utc = '2026-07-01T12:30:00Z';
console.log({
  naive_timestamp: new Date(naive).getTime(),
  naive_iso: new Date(naive).toISOString(),
  naive_locale: new Date(naive).toLocaleString(),
  utc_timestamp: new Date(utc).getTime(),
  utc_iso: new Date(utc).toISOString(),
  utc_locale: new Date(utc).toLocaleString(),
});
console.log('same timestamp naive==utc?', new Date(naive).getTime() === new Date(utc).getTime());
JS

Repository: Resgrid/Unit

Length of output: 411


Sanitize the planned routes timestamp fields before formatting.

These values are typed as plain strings, while PlannedArrival/PlannedDeparture are also rendered raw in stop-card.tsx and via new Date() in routes/start.tsx; that leaves the contract ambiguous and can parse timezone-less values as local time. Add a typed UTC designator or validate the contract, and apply the same parsing in stop-card.tsx and routes/start.tsx.

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

In `@src/app/routes/stop/`[id].tsx around lines 218 - 222, Normalize
PlannedArrival and PlannedDeparture to an explicit UTC timestamp contract before
passing them to safeFormatDate, and update the corresponding raw rendering in
stop-card.tsx and new Date() usage in routes/start.tsx to use the same parsing.
Preserve the existing display behavior while ensuring timezone-less values are
not interpreted as local time.

Comment on lines +4 to 8
import { logger } from '@/lib/logging';
import { subscribeToSharedTicker } from '@/lib/shared-ticker';
import type { CheckInTimerStatusResultData } from '@/models/v4/checkIn/checkInTimerStatusResultData';

import { CheckInTimerCard } from '../check-in-timer-card';

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

Move the type-only import after relative value imports.

The type import on Line 6 precedes the relative component import on Line 8. As per coding guidelines, ESLint enforces simple-import-sort with grouping order: side-effect imports, external packages, internal aliases, relative imports, type 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/check-in-timers/__tests__/check-in-timer-card.test.tsx` around
lines 4 - 8, Reorder the imports in the check-in timer card test so the relative
value import of CheckInTimerCard appears before the type-only
CheckInTimerStatusResultData import, preserving the existing side-effect,
external, and alias import groups.

Source: Coding guidelines

Comment on lines +453 to +535
const reconnectTimer = setTimeout(async () => {
if (this.reconnectTimers.get(hubName) === reconnectTimer) {
this.reconnectTimers.delete(hubName);
}

try {
// Check if the hub config was removed (e.g., by explicit disconnect)
const currentHubConfig = this.hubConfigs.get(hubName);
if (!currentHubConfig) {
logger.debug({
message: `Hub ${hubName} config was removed, skipping reconnection attempt`,
});
return;
}

// If a live connection exists, skip; if it's stale/closed, drop it
const existingConn = this.connections.get(hubName);
if (existingConn && existingConn.state === HubConnectionState.Connected) {
logger.debug({
message: `Hub ${hubName} is already connected, skipping reconnection attempt`,
});
this.reconnectAttempts.set(hubName, 0);
return;
}

// Mark as reconnecting and remove stale entry (if any) to allow a fresh connect
this.setHubState(hubName, HubConnectingState.RECONNECTING);
if (existingConn) {
this.connections.delete(hubName);
}

// Refresh authentication token before reconnecting
logger.info({
message: `Refreshing authentication token before reconnecting to hub: ${hubName}`,
});

await useAuthStore.getState().refreshAccessToken();

// Verify we have a valid token after refresh — if the session is gone
// (refresh rejected), stop reconnecting instead of retrying forever.
const token = useAuthStore.getState().accessToken;
if (!token) {
logger.warn({
message: `No valid authentication token after refresh, aborting reconnect for hub: ${hubName}`,
});
this.setHubState(hubName, HubConnectingState.IDLE);
this.reconnectAttempts.delete(hubName);
this.hubConfigs.delete(hubName);
return;
}

logger.info({
message: `Token refreshed successfully, attempting to reconnect to hub: ${hubName} (attempt ${currentAttempts}/${this.MAX_RECONNECT_ATTEMPTS})`,
});

this.connections.delete(hubName);

await this.connectToHubWithEventingUrl(currentHubConfig);

// Clear reconnecting state on successful reconnection
this.setHubState(hubName, HubConnectingState.IDLE);
this.reconnectAttempts.set(hubName, 0);

logger.info({
message: `Successfully reconnected to hub: ${hubName} after ${currentAttempts} attempts`,
});
} catch (error) {
// Attempt failed — the old connection object is gone, so no further
// onclose event will fire. We MUST reschedule here or the hub stays
// dead until the next app background/resume cycle.
this.setHubState(hubName, HubConnectingState.IDLE);

logger.error({
message: `Reconnection attempt ${currentAttempts}/${this.MAX_RECONNECT_ATTEMPTS} failed for hub: ${hubName}`,
context: { error },
});

// Reschedule (attempt count was already incremented when this attempt
// was scheduled); handleConnectionClose enforces the max-attempt cap.
this.handleConnectionClose(hubName);
}
}, delay);
this.reconnectTimers.set(hubName, reconnectTimer);

Copy link
Copy Markdown

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

An already-running reconnect callback can resurrect a hub after an explicit disconnect.

clearReconnectTimer only cancels a pending setTimeout. Once the callback body starts, it reads hubConfigs at Line 460 and then awaits refreshAccessToken() (Line 489) and connectToHubWithEventingUrl() (Line 510). If disconnectFromHub/disconnectAll runs during those awaits, it deletes hubConfigs/connections only after stop() resolves, so the in-flight callback re-registers the config and re-opens the connection — a hub that stays connected after teardown (and, in resetInstance/logout paths, keeps a socket alive under a stale session).

Suggest a per-hub generation counter (or a disconnectedHubs epoch set) checked after each await before proceeding.

🛡️ Sketch
+        // Re-check after the async gap — an explicit disconnect may have run.
+        if (!this.hubConfigs.has(hubName)) {
+          logger.debug({ message: `Hub ${hubName} disconnected during reconnect, aborting` });
+          this.setHubState(hubName, HubConnectingState.IDLE);
+          return;
+        }
         logger.info({
           message: `Token refreshed successfully, attempting to reconnect to hub: ${hubName} (attempt ${currentAttempts}/${this.MAX_RECONNECT_ATTEMPTS})`,
         });
🤖 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 453 - 535, Update the reconnect
callback around reconnectTimers and connectToHubWithEventingUrl to invalidate
in-flight reconnects when disconnectFromHub, disconnectAll, or resetInstance
tears down a hub. Add a per-hub generation/epoch (or equivalent disconnected
marker), capture it when scheduling the callback, and verify it after
refreshAccessToken and before reconnecting or applying success state; abort when
the generation changed or hub configuration was removed. Ensure teardown
increments or marks the generation so the callback cannot recreate configuration
or reopen a connection.

Comment on lines +86 to +89
Platform: {
OS: 'web',
},
}));

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

stopForegroundService assertion is vacuous under Platform.OS: 'web'.

The unexpected-drop handler only calls notifee.stopForegroundService() inside an android branch, so Line 243 would pass even if the intentional-switch guard regressed. Set Platform.OS: 'android' (or add a second case) so this assertion actually covers the foreground-service teardown path.

Also applies to: 243-243

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

In `@src/stores/app/__tests__/livekit-store-room-switch.test.ts` around lines 86 -
89, Update the test setup in livekit-store-room-switch.test.ts so the
stopForegroundService assertion exercises the Android-specific branch by setting
Platform.OS to "android" for that case. Preserve the existing intentional-switch
guard coverage and ensure the assertion would fail if foreground-service
teardown were incorrectly triggered.

Comment on lines +585 to +592
room.on(RoomEvent.Disconnected, () => {
// Fired on network drop AND on intentional room.disconnect(). The
// intentional path sets isConnected=false BEFORE disconnecting, so a
// truthy value here means an unexpected drop — reset state so the UI
// never gets stuck showing "in call" with a dead room.
if (!get().isConnected) {
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard on room identity, not just isConnected.

This handler is registered per-room but only checks the global isConnected flag. A late Disconnected from a previously torn-down room (LiveKit can emit it after disconnect() resolves) will observe isConnected === true for the new room and wipe currentRoom/CallKeep/foreground-service state for a live call. Add an identity check.

🛡️ Proposed fix
       room.on(RoomEvent.Disconnected, () => {
-        if (!get().isConnected) {
+        // Ignore events from a room that is no longer the active one.
+        if (get().currentRoom !== room || !get().isConnected) {
           return;
         }
📝 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
room.on(RoomEvent.Disconnected, () => {
// Fired on network drop AND on intentional room.disconnect(). The
// intentional path sets isConnected=false BEFORE disconnecting, so a
// truthy value here means an unexpected drop — reset state so the UI
// never gets stuck showing "in call" with a dead room.
if (!get().isConnected) {
return;
}
room.on(RoomEvent.Disconnected, () => {
// Fired on network drop AND on intentional room.disconnect(). The
// intentional path sets isConnected=false BEFORE disconnecting, so a
// truthy value here means an unexpected drop — reset state so the UI
// never gets stuck showing "in call" with a dead room.
// Ignore events from a room that is no longer the active one.
if (get().currentRoom !== room || !get().isConnected) {
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/stores/app/livekit-store.ts` around lines 585 - 592, Update the
RoomEvent.Disconnected handler to verify that the event’s room is still the
store’s active/current room before resetting call state. Keep the existing
isConnected guard, but add a room-identity check using the room instance
captured by this handler and the store’s current room reference, returning early
for stale rooms so currentRoom, CallKeep, and foreground-service state remain
intact.

Comment thread src/stores/auth/store.tsx
Comment on lines +339 to +350
// Only persist what is needed to restore a session. Transient fields —
// status, error, refreshTimeoutId — must never be persisted: an app kill
// mid-login used to rehydrate `status: 'loading'` with no way out,
// permanently locking the user behind a spinner.
partialize: (state) => ({
accessToken: state.accessToken,
refreshToken: state.refreshToken,
refreshTokenExpiresOn: state.refreshTokenExpiresOn,
profile: state.profile,
userId: state.userId,
isFirstTime: state.isFirstTime,
}),

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 | 🟠 Major | 🏗️ Heavy lift

Offline cold start can never reach signedIn.

status is no longer persisted, so after rehydration the only path to signedIn is a successful refreshAccessToken(). Line 377 sets status: 'loading' and, on a transient failure (offline — the common case for this app), Line 253 just retries every 30s and leaves status: 'loading' forever. Since isAuthenticated() requires status === 'signedIn', a responder who launches the app without connectivity is locked out of cached data, which is a regression from persisting status.

Consider optimistically setting status: 'signedIn' on rehydration when a non-expired accessToken exists (using the persisted refreshTokenExpiresOn), and reserving loading for the case where only a refresh token is present.

Also applies to: 363-383

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

In `@src/stores/auth/store.tsx` around lines 339 - 350, Update the auth store
rehydration flow around partialize and refreshAccessToken so a persisted,
non-expired accessToken immediately restores status to signedIn, using
refreshTokenExpiresOn for expiry validation. Set status to loading only when
rehydration has a refresh token without a usable access token, while preserving
the existing refresh retry behavior for transient failures.

Comment on lines +1 to +48
import { act, renderHook } from '@testing-library/react-native';

const mockCoreStoreGetState = jest.fn(() => ({ config: { EventingUrl: 'https://eventing.example.com/' } }));
const mockSecurityStore = { getState: jest.fn(() => ({ rights: { DepartmentId: '123' } })) };

jest.mock('@/services/signalr.service', () => {
const mockInstance = {
connectToHubWithEventingUrl: jest.fn().mockResolvedValue(undefined),
disconnectFromHub: jest.fn().mockResolvedValue(undefined),
invoke: jest.fn().mockResolvedValue(undefined),
on: jest.fn(),
removeAllListeners: jest.fn(),
};
class MockSignalRService {
static readonly HUB_DISCONNECTED_EVENT = '__hubDisconnected';
static readonly HUB_RECONNECTING_EVENT = '__hubReconnecting';
static readonly HUB_RECONNECTED_EVENT = '__hubReconnected';
}
return { SignalRService: MockSignalRService, signalRService: mockInstance, default: mockInstance };
});
jest.mock('../../app/core-store', () => {
const mockStore: any = () => mockCoreStoreGetState();
mockStore.getState = () => mockCoreStoreGetState();
return { useCoreStore: mockStore };
});
jest.mock('../../security/store', () => {
console.log('RELATIVE security mock factory ran');
return { securityStore: mockSecurityStore, useSecurityStore: mockSecurityStore };
});
jest.mock('@/stores/security/store', () => {
console.log('ALIAS security mock factory ran');
return { securityStore: mockSecurityStore };
});
jest.mock('@/lib/logging', () => ({ logger: { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(), trace: jest.fn(), fatal: jest.fn() } }));
jest.mock('@/lib/env', () => ({ Env: { CHANNEL_HUB_NAME: 'eventingHub', REALTIME_GEO_HUB_NAME: 'geolocationHub' } }));
jest.mock('@/lib', () => ({ useAuthStore: { getState: jest.fn(() => ({ accessToken: 'mock-token' })) } }));

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

it('dbg join', async () => {
const { result } = renderHook(() => useSignalRStore());
await act(async () => {
await result.current.connectUpdateHub();
});
console.log('error:', result.current.error);
console.log('invoke calls:', (signalRService.invoke as jest.Mock).mock.calls);
});

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 | 🟠 Major | ⚡ Quick win

Delete this debug artifact.

zz-dbg.test.ts is a scratch file: it has no assertions (so it can never fail), prints via console.log at Lines 27, 31, 46 and 47, duplicates mock setup and the department-join scenario already covered by src/stores/signalr/__tests__/signalr-store.test.ts (Lines 221-229), and the zz-dbg name does not describe anything under test. It only adds CI time and maintenance noise.

As per coding guidelines, "Never use console.log in production code — always use logger from @/lib/logging" and "Use lowercase-hyphenated naming for files and directories".

🤖 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/__tests__/zz-dbg.test.ts` around lines 1 - 48, Delete the
scratch test artifact zz-dbg.test.ts entirely; its console logging, duplicated
mocks, and assertion-free department-join scenario are already covered by
signalr-store.test.ts.

Source: Coding guidelines

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