Skip to content

RG-T117 chat feature flag - #262

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

RG-T117 chat feature flag#262
ucswift merged 2 commits into
masterfrom
develop

Conversation

@ucswift

@ucswift ucswift commented Aug 8, 2026

Copy link
Copy Markdown
Member

This pull request introduces a feature flag system and uses it to gate the entire chat feature behind a department-scoped Chat.System toggle.

Feature flag infrastructure:

  • Adds an API client for the v4 FeatureToggles endpoint with methods to fetch all flags or check a single flag's state.
  • Adds a persisted Zustand store that fetches and caches feature flags at app startup, retaining cached values on failure for stable offline gating. Includes reactive hooks (useFeatureFlag, useIsChatEnabled) for components.

Chat gating by feature flag:

  • App initialization now fetches feature flags and only connects the SignalR chat hub when Chat.System is enabled for the department.
  • The sidebar hides the Chat and Assistant navigation buttons when the flag is off.
  • All chat-related screens (chat list, chatbot/assistant, channel conversation, and thread view) redirect to the home screen when the flag is disabled, also blocking deep-link access.
  • Data-fetching effects in those screens are short-circuited when chat is disabled to avoid unnecessary API calls.

In summary, when the Chat.System feature flag is disabled for a department, the app fully hides and deactivates all chat functionality—navigation entry points, screen access, data loading, and realtime hub connections.

Summary by CodeRabbit

  • New Features
    • Added feature-flag support with persisted, identity-aware settings.
    • Chat and Assistant features can now be enabled or disabled remotely.
  • User Experience
    • Disabled chat-related screens and navigation are hidden or redirected.
    • Loading indicators appear while feature availability is being determined.
    • Chat connections are skipped when chat is disabled.
  • Bug Fixes
    • Improved logout and account-switch handling by clearing feature settings appropriately.
  • Tests
    • Added coverage for feature-flag loading, persistence, identity changes, resets, and chat availability.

@Resgrid-Bot

This comment has been minimized.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds typed feature-flag API helpers and an identity-aware persisted Zustand store. App startup loads flags and gates SignalR connection. Chat screens, routes, data loading, chatbot initialization, sidebar navigation, and app reset use feature-flag state.

Changes

Chat feature-flag gating

Layer / File(s) Summary
Feature-flag API, store, and validation
src/api/feature-flags/feature-flags.ts, src/stores/feature-flags/store.ts, src/stores/feature-flags/__tests__/store.test.ts
Adds typed API responses, identity-aware persistence, status hooks, failure handling, and store tests.
App initialization and chat connection
src/app/(app)/_layout.tsx, src/stores/signalr/signalr-store.ts, src/stores/signalr/__tests__/*
Loads feature flags during startup and connects to the chat hub only when ChatSystem is enabled.
Chat route and navigation gating
src/app/(app)/chat.tsx, src/app/(app)/chatbot.tsx, src/app/chat/[channelId].tsx, src/app/chat/thread/[messageId].tsx, src/components/sidebar/sidebar-content.tsx
Shows loading states while status is unresolved, skips chat work when disabled, redirects disabled routes, and hides chat navigation.
Feature-flag reset integration
src/services/app-reset.service.ts, src/services/__tests__/app-reset.service.test.ts
Resets feature-flag state and verifies feature-flag and chat reset behavior.

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

Sequence Diagram(s)

sequenceDiagram
  participant AppLayout
  participant featureFlagsStore
  participant ChatSignalR
  participant ChatRoute
  participant Sidebar
  AppLayout->>featureFlagsStore: fetch ChatSystem flag
  featureFlagsStore-->>AppLayout: return feature-flag status
  AppLayout->>ChatSignalR: connect when enabled
  ChatRoute->>featureFlagsStore: read ChatSystem status
  ChatRoute->>ChatSignalR: load chat data when enabled
  Sidebar->>featureFlagsStore: read ChatSystem status
  Sidebar-->>Sidebar: render chat navigation when enabled
Loading

Possibly related PRs

  • Resgrid/Unit#260: Adds the chat entry points that this PR gates with ChatSystem.

Suggested reviewers: github-actions

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding the RG-T117 chat feature flag and its related behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (3)
src/stores/feature-flags/store.ts (1)

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

Use the configured source alias.

Replace the relative storage import with @/lib/storage.

Proposed fix
-import { zustandStorage } from '../../lib/storage';
+import { zustandStorage } from '`@/lib/storage`';

As per coding guidelines, use path aliases from tsconfig.json instead of relative paths in src/.

🤖 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/feature-flags/store.ts` at line 7, Update the import in the
feature-flags store to use the configured `@/lib/storage` alias instead of the
relative ../../lib/storage path, leaving the imported zustandStorage symbol
unchanged.

Source: Coding guidelines

src/api/feature-flags/feature-flags.ts (1)

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

Use the required API endpoint factory.

These functions call api.get() directly. Use createApiEndpoint or createCachedApiEndpoint with typed response generics. This preserves the API-module contract for request handling and caching.

🤖 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/api/feature-flags/feature-flags.ts` around lines 28 - 36, Update
getAllFeatureFlags and getFeatureFlagState to use the required createApiEndpoint
or createCachedApiEndpoint factories with their existing typed response generics
instead of calling api.get directly. Preserve the current endpoint paths, query
parameters, abort signal handling, and return values.

Source: Coding guidelines

src/components/sidebar/sidebar-content.tsx (1)

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

Use a ternary for conditional rendering.

Replace the && expression with isChatEnabled ? <HStack ... /> : null.

Proposed fix
-        {isChatEnabled && (
+        {isChatEnabled ? (
           <HStack space="md">
             <Button variant="outline" action="secondary" size="md" className="flex-1" onPress={handleNavigateToChat}>
               <MessagesSquare size={18} color="`#2563eb`" />
               <ButtonText className="ml-2">{t('tabs.chat')}</ButtonText>
             </Button>
             <Button variant="outline" action="secondary" size="md" className="flex-1" onPress={handleNavigateToAssistant}>
               <Sparkles size={18} color="`#7c3aed`" />
               <ButtonText className="ml-2">{t('tabs.assistant')}</ButtonText>
             </Button>
           </HStack>
-        )}
+        ) : null}

As per coding guidelines, use the conditional operator for conditional rendering instead of &&.

🤖 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/sidebar/sidebar-content.tsx` around lines 68 - 80, Update the
conditional rendering in the sidebar content around isChatEnabled to use a
ternary expression that renders the existing HStack when enabled and null
otherwise, preserving the current navigation buttons and handlers.

Source: Coding guidelines

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

Inline comments:
In `@src/app/`(app)/_layout.tsx:
- Around line 168-189: The useSignalRLifecycle chat-hub lifecycle bypasses the
Chat.System feature gate. Update useSignalRLifecycle so resume connects ChatHub
and background disconnects include ChatHub only when featureFlagsStore reports
FeatureFlagKeys.ChatSystem enabled; preserve existing lifecycle handling for the
other hubs.

In `@src/app/`(app)/chatbot.tsx:
- Around line 33-41: Gate all chat lifecycle effects on isChatEnabled. In
src/app/(app)/chatbot.tsx lines 33-41, ensure the active-channel focus effect
skips both initialization and cleanup setActiveChannel mutations when disabled.
In src/app/chat/[channelId].tsx lines 70-81, add isChatEnabled guards to the
presence and mark-read effects, and cancel any in-flight presence loading when
chat is disabled.

In `@src/stores/feature-flags/store.ts`:
- Around line 29-59: Scope the persisted cache in featureFlagsStore to the
authenticated department so flags cannot bleed across department changes or
sign-out; use department identity in the persistence key or clear the store
during those transitions. Configure persist with partialize to retain only
cacheable flags, excluding isLoaded and error, while preserving the existing
failure behavior for the in-memory flags.
- Around line 63-67: The useFeatureFlag and fetchFlags flow must distinguish
unresolved flags from an explicitly disabled flag. Expose the current flag value
together with a resolved/loading state, set that state on both successful and
failed fetches, and update useIsChatEnabled consumers so chat redirects and
effects wait until the current department flag load has resolved.

---

Nitpick comments:
In `@src/api/feature-flags/feature-flags.ts`:
- Around line 28-36: Update getAllFeatureFlags and getFeatureFlagState to use
the required createApiEndpoint or createCachedApiEndpoint factories with their
existing typed response generics instead of calling api.get directly. Preserve
the current endpoint paths, query parameters, abort signal handling, and return
values.

In `@src/components/sidebar/sidebar-content.tsx`:
- Around line 68-80: Update the conditional rendering in the sidebar content
around isChatEnabled to use a ternary expression that renders the existing
HStack when enabled and null otherwise, preserving the current navigation
buttons and handlers.

In `@src/stores/feature-flags/store.ts`:
- Line 7: Update the import in the feature-flags store to use the configured
`@/lib/storage` alias instead of the relative ../../lib/storage path, leaving the
imported zustandStorage symbol unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1606ed4c-faf9-4235-adfd-03f11d8cdf0e

📥 Commits

Reviewing files that changed from the base of the PR and between da21262 and 00dcaa3.

📒 Files selected for processing (8)
  • src/api/feature-flags/feature-flags.ts
  • src/app/(app)/_layout.tsx
  • src/app/(app)/chat.tsx
  • src/app/(app)/chatbot.tsx
  • src/app/chat/[channelId].tsx
  • src/app/chat/thread/[messageId].tsx
  • src/components/sidebar/sidebar-content.tsx
  • src/stores/feature-flags/store.ts

Comment thread src/app/(app)/_layout.tsx
Comment on lines +168 to 189
await Promise.all([useRolesStore.getState().init(), useCallsStore.getState().init(), useWeatherAlertsStore.getState().init(), securityStore.getState().getRights(), featureFlagsStore.getState().fetchFlags()]);

// SignalR needs config (EventingUrl) and rights (DepartmentId) — both
// available now. The two hub connects are independent.
await Promise.all([useSignalRStore.getState().connectUpdateHub(), useSignalRStore.getState().connectGeolocationHub()]);

// Connect the realtime chat hub (best-effort; chat may be disabled per department)
try {
await useSignalRStore.getState().connectChatHub();
} catch (error) {
logger.warn({
message: 'Failed to connect SignalR chat hub during initialization',
context: { error, platform: Platform.OS },
// Connect the realtime chat hub only when the Chat.System feature flag is on for
// this department; when it is off every chat surface stays hidden.
if (featureFlagsStore.getState().isEnabled(FeatureFlagKeys.ChatSystem)) {
// Best-effort: chat hub failure should not block app initialization.
try {
await useSignalRStore.getState().connectChatHub();
} catch (error) {
logger.warn({
message: 'Failed to connect SignalR chat hub during initialization',
context: { error, platform: Platform.OS },
});
}
} else {
logger.info({
message: 'Chat disabled by feature flag; skipping chat hub connection',
});

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect every chat-hub connection call and lifecycle implementation.
rg -nP -C 6 '\bconnectChatHub\s*\(' src
rg -nP -C 8 '\buseSignalRLifecycle\s*\(' src

Repository: Resgrid/Unit

Length of output: 24296


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== use-signalr-lifecycle.ts relevant sections =="
sed -n '1,170p' src/hooks/use-signalr-lifecycle.ts
echo
echo "== signalr store files =="
fd -a 'signalr-store|signalr' src/stores src/components src/app src/hooks | sed 's#^\./##' | head -80
echo
echo "== chat disable/feature flag references =="
rg -n -C 4 'ChatSystem|connectChatHub|disconnectChatHub|isChatHubConnected|hubNames|ChatHub' src

Repository: Resgrid/Unit

Length of output: 19189


Prevent useSignalRLifecycle from reconnecting the chat hub when Chat.System is disabled.

useSignalRLifecycle runs connectChatHub() on resume and includes ChatHub in disconnects during background. That bypasses the Chat.System gate used in _layout.tsx; only add/remove the chat hub when the feature flag is true.

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

In `@src/app/`(app)/_layout.tsx around lines 168 - 189, The useSignalRLifecycle
chat-hub lifecycle bypasses the Chat.System feature gate. Update
useSignalRLifecycle so resume connects ChatHub and background disconnects
include ChatHub only when featureFlagsStore reports FeatureFlagKeys.ChatSystem
enabled; preserve existing lifecycle handling for the other hubs.

Comment thread src/app/(app)/chatbot.tsx
Comment on lines +29 to +59
export const featureFlagsStore = create<FeatureFlagsState>()(
persist(
(set, get) => ({
flags: {},
isLoaded: false,
error: null,
fetchFlags: async () => {
try {
const response = await getAllFeatureFlags();
const flags: Record<string, FeatureFlagEntry> = {};
for (const flag of response?.Data ?? []) {
if (flag?.Key) {
flags[flag.Key] = { enabled: !!flag.Enabled, value: flag.Value ?? null };
}
}
set({ flags, isLoaded: true, error: null });
} catch (error) {
// Keep any persisted flags on failure so gating stays stable while offline.
logger.error({
message: 'Failed to fetch feature flags',
context: { error },
});
set({ error: error instanceof Error ? error.message : 'Failed to fetch feature flags' });
}
},
isEnabled: (key: string, defaultValue = false) => get().flags[key]?.enabled ?? defaultValue,
}),
{
name: 'feature-flags-storage',
storage: createJSONStorage(() => zustandStorage),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

Scope persisted flags to the authenticated department.

Line 57 stores department-scoped flags under one device-wide key. Line 46 retains those values when refresh fails. A user who changes departments can then inherit the prior department's Chat.System value, which can expose chat navigation and start the chat hub incorrectly.

Clear this state on sign-out and department changes, or namespace the persisted cache by department identity. Persist only cacheable flag data with partialize; do not persist isLoaded or error.

As per coding guidelines, Zustand persistence must use partialize to exclude transient state such as loading and error flags.

🤖 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/feature-flags/store.ts` around lines 29 - 59, Scope the persisted
cache in featureFlagsStore to the authenticated department so flags cannot bleed
across department changes or sign-out; use department identity in the
persistence key or clear the store during those transitions. Configure persist
with partialize to retain only cacheable flags, excluding isLoaded and error,
while preserving the existing failure behavior for the in-memory flags.

Source: Coding guidelines

Comment on lines +63 to +67
// Reactive hook; components re-render when the flag changes. Unknown flags default to disabled
// so gated features stay hidden until the server confirms them.
export const useFeatureFlag = (key: string, defaultValue = false) => featureFlagsStore((state) => state.flags[key]?.enabled ?? defaultValue);

export const useIsChatEnabled = () => useFeatureFlag(FeatureFlagKeys.ChatSystem);

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

Do not treat an unresolved flag as disabled.

useFeatureFlag() returns false before fetchFlags() completes. The app layout fetches flags during asynchronous initialization, while the chat routes immediately redirect when this hook returns false. A direct link to an enabled chat route can therefore redirect to /(app) before the current department flags load.

Expose a resolved state with the flag value. Defer chat redirects and chat effects until the current flag load has resolved. Set that state on both successful and failed fetches.

🤖 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/feature-flags/store.ts` around lines 63 - 67, The useFeatureFlag
and fetchFlags flow must distinguish unresolved flags from an explicitly
disabled flag. Expose the current flag value together with a resolved/loading
state, set that state on both successful and failed fetches, and update
useIsChatEnabled consumers so chat redirects and effects wait until the current
department flag load has resolved.


/** Evaluates every active flag for the caller's department. */
export const getAllFeatureFlags = async (signal?: AbortSignal) => {
const response = await api.get<FeatureTogglesResult>(`${FEATURE_TOGGLES}/GetAll`, { signal });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unhandled rejection: the awaited api.get call at src/api/feature-flags/feature-flags.ts:35 propagates network failures, 5xx responses, and aborts to callers with no context. Wrap the await in try/catch (or attach .catch), log the error, and rethrow or return a safe default.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File src/api/feature-flags/feature-flags.ts:

Line 29:

Unhandled rejection: the awaited api.get call at src/api/feature-flags/feature-flags.ts:35 propagates network failures, 5xx responses, and aborts to callers with no context. Wrap the await in try/catch (or attach .catch), log the error, and rethrow or return a safe default.

Talk to Kody by mentioning @kody

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


/** Evaluates every active flag for the caller's department. */
export const getAllFeatureFlags = async (signal?: AbortSignal) => {
const response = await api.get<FeatureTogglesResult>(`${FEATURE_TOGGLES}/GetAll`, { signal });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unhandled network exception: api.get at src/api/feature-flags/feature-flags.ts:35 throws transient and non-transient errors that propagate without enrichment or mapping to application-level errors. Wrap the call in try/catch, log with structured context including the flag operation name, and rethrow or return a typed error.

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

Prompt for LLM

File src/api/feature-flags/feature-flags.ts:

Line 29:

Unhandled network exception: api.get at src/api/feature-flags/feature-flags.ts:35 throws transient and non-transient errors that propagate without enrichment or mapping to application-level errors. Wrap the call in try/catch, log with structured context including the flag operation name, and rethrow or return a typed error.

Talk to Kody by mentioning @kody

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

};

/** Lightweight enabled-only check for a single flag. */
export const getFeatureFlagState = async (key: string, signal?: AbortSignal) => {

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 Promise contract documentation: getFeatureFlagState at src/api/feature-flags/feature-flags.ts:28 omits @returns and rejection conditions, leaving callers without resolve/reject semantics for correct await usage. Add @returns {Promise<FeatureToggleResult|undefined>} and document rejection conditions (network errors, unknown key, aborts).

Kody rule violation: Document async/Promise behavior and errors

Prompt for LLM

File src/api/feature-flags/feature-flags.ts:

Line 34:

Missing Promise contract documentation: getFeatureFlagState at src/api/feature-flags/feature-flags.ts:28 omits @returns and rejection conditions, leaving callers without resolve/reject semantics for correct await usage. Add `@returns {Promise<FeatureToggleResult|undefined>}` and document rejection conditions (network errors, unknown key, aborts).

Talk to Kody by mentioning @kody

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

Comment thread src/app/(app)/_layout.tsx
// These fetches are independent of each other — run in parallel to cut
// time-to-interactive (previously 8+ serial network hops).
await Promise.all([useRolesStore.getState().init(), useCallsStore.getState().init(), useWeatherAlertsStore.getState().init(), securityStore.getState().getRights()]);
await Promise.all([useRolesStore.getState().init(), useCallsStore.getState().init(), useWeatherAlertsStore.getState().init(), securityStore.getState().getRights(), featureFlagsStore.getState().fetchFlags()]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Promise.all misuse for independent batch operations violates the team's partial-failure handling rule: if any single init() rejects (useRolesStore, useCallsStore, useWeatherAlertsStore, securityStore, or featureFlagsStore), the remaining initializations are aborted. Replace Promise.all with Promise.allSettled and handle per-item results.

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

Prompt for LLM

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

Line 168:

Promise.all misuse for independent batch operations violates the team's partial-failure handling rule: if any single init() rejects (useRolesStore, useCallsStore, useWeatherAlertsStore, securityStore, or featureFlagsStore), the remaining initializations are aborted. Replace Promise.all with Promise.allSettled and handle per-item results.

Talk to Kody by mentioning @kody

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

isEnabled: (key: string, defaultValue?: boolean) => boolean;
}

export const featureFlagsStore = create<FeatureFlagsState>()(

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

Stale state leak across user sessions: featureFlagsStore is omitted from resetAllStores() in src/services/app-reset.service.ts (lines 258-319), so the previous user's flags persist in-memory after logout and briefly surface for the next user until fetchFlags() completes during re-login. Add featureFlagsStore.setState({ flags: {}, isLoaded: false, error: null }) alongside the existing department-scoped store resets at lines 310-314.

// In app-reset.service.ts resetAllStores(), alongside other department-scoped resets:
// import { featureFlagsStore } from '@/stores/feature-flags/store';
// featureFlagsStore.setState({ flags: {}, isLoaded: false, error: null });
Prompt for LLM

File src/stores/feature-flags/store.ts:

Line 29:

Stale state leak across user sessions: featureFlagsStore is omitted from resetAllStores() in src/services/app-reset.service.ts (lines 258-319), so the previous user's flags persist in-memory after logout and briefly surface for the next user until fetchFlags() completes during re-login. Add `featureFlagsStore.setState({ flags: {}, isLoaded: false, error: null })` alongside the existing department-scoped store resets at lines 310-314.

Suggested Code:

// In app-reset.service.ts resetAllStores(), alongside other department-scoped resets:
// import { featureFlagsStore } from '@/stores/feature-flags/store';
// featureFlagsStore.setState({ flags: {}, isLoaded: false, error: null });

Talk to Kody by mentioning @kody

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

isEnabled: (key: string, defaultValue?: boolean) => boolean;
}

export const featureFlagsStore = create<FeatureFlagsState>()(

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

Cross-account isolation violation: featureFlagsStore is omitted from resetAllStores() (src/services/app-reset.service.ts:258), and since logout performs an in-place navigation without a JS bundle reload, the previous user's flags persist in module-level zustand state — surfacing disabled features (e.g., Chat.System) to the next user if their fetchFlags() fails offline. Export an INITIAL_FEATURE_FLAGS_STATE constant and call featureFlagsStore.setState() alongside the other department-scoped resets to restore the auth/store.tsx:172-174 isolation guarantee.

// In app-reset.service.ts resetAllStores(), alongside the other department-scoped stores:
// featureFlagsStore.setState({ flags: {}, isLoaded: false, error: null });
//
// To keep it encapsulated, add to the store:
//   export const INITIAL_FEATURE_FLAGS_STATE = { flags: {}, isLoaded: false, error: null };
// and call featureFlagsStore.setState(INITIAL_FEATURE_FLAGS_STATE); during reset.
Prompt for LLM

File src/stores/feature-flags/store.ts:

Line 29:

Cross-account isolation violation: featureFlagsStore is omitted from resetAllStores() (src/services/app-reset.service.ts:258), and since logout performs an in-place navigation without a JS bundle reload, the previous user's flags persist in module-level zustand state — surfacing disabled features (e.g., Chat.System) to the next user if their fetchFlags() fails offline. Export an INITIAL_FEATURE_FLAGS_STATE constant and call featureFlagsStore.setState() alongside the other department-scoped resets to restore the auth/store.tsx:172-174 isolation guarantee.

Suggested Code:

// In app-reset.service.ts resetAllStores(), alongside the other department-scoped stores:
// featureFlagsStore.setState({ flags: {}, isLoaded: false, error: null });
//
// To keep it encapsulated, add to the store:
//   export const INITIAL_FEATURE_FLAGS_STATE = { flags: {}, isLoaded: false, error: null };
// and call featureFlagsStore.setState(INITIAL_FEATURE_FLAGS_STATE); during reset.

Talk to Kody by mentioning @kody

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

Comment on lines +47 to +50
logger.error({
message: 'Failed to fetch feature flags',
context: { error },
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Unstructured log fields: the error log embeds the operation name inside the message string and omits structured identifiers (trace ID, flag keys), violating Rule 3's SIEM-searchability requirement. Add a dedicated operation field and include identifiers — e.g., logger.error({ message: 'Failed to fetch feature flags', operation: 'fetchFlags', context: { error, traceId } }).

Kody rule violation: Include error context in structured logs

Prompt for LLM

File src/stores/feature-flags/store.ts:

Line 47 to 50:

Unstructured log fields: the error log embeds the operation name inside the message string and omits structured identifiers (trace ID, flag keys), violating Rule 3's SIEM-searchability requirement. Add a dedicated `operation` field and include identifiers — e.g., `logger.error({ message: 'Failed to fetch feature flags', operation: 'fetchFlags', context: { error, traceId } })`.

Talk to Kody by mentioning @kody

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

@Resgrid-Bot

Resgrid-Bot commented Aug 8, 2026

Copy link
Copy Markdown

Code Review Completed! 🔥

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

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

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

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

Current Kody Configuration
Review Options

The following review options are enabled or disabled:

Options Enabled
Bug
Performance
Security
Business Logic

Access your configuration settings here.

@coderabbitai coderabbitai Bot 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: 1

🧹 Nitpick comments (2)
src/stores/feature-flags/__tests__/store.test.ts (1)

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

Unmount each hook test.

Each renderHook() call leaves the mounted hook active. Destructure unmount and call it after the assertions in each test.

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

🤖 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/feature-flags/__tests__/store.test.ts` around lines 175 - 211,
Update each useChatSystemStatus test to destructure unmount from its renderHook
result and call unmount after the assertions; in the enabled/disabled test,
unmount each separately rendered hook before or after updating state while
preserving the existing assertions.

Source: Coding guidelines

src/stores/feature-flags/store.ts (1)

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

Use configured aliases for these source-module references.

Replace relative module specifiers with the configured @/* aliases.

  • src/stores/feature-flags/store.ts#L8-L9: import the auth and security stores through @/stores/....
  • src/stores/signalr/signalr-store.ts#L10-L10: import the feature-flag store through @/stores/feature-flags/store.
  • src/stores/feature-flags/__tests__/store.test.ts#L3-L3: import the feature-flag store through its @/stores/... alias.
  • src/stores/feature-flags/__tests__/store.test.ts#L21-L21: mock storage through @/lib/storage.
  • src/stores/feature-flags/__tests__/store.test.ts#L30-L30: mock auth through @/stores/auth/store.
  • src/stores/feature-flags/__tests__/store.test.ts#L37-L37: mock security through @/stores/security/store.
  • src/stores/feature-flags/__tests__/store.test.ts#L43-L45: retrieve mocks through the same alias specifiers.

As per coding guidelines, “Use path aliases from tsconfig.json (@/*, @env, @assets/*) instead of relative paths.”

🤖 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/feature-flags/store.ts` around lines 8 - 9, Replace the relative
imports and mock references with configured `@/`* aliases: in
src/stores/feature-flags/store.ts:8-9 use `@/stores/auth/store` and
`@/stores/security/store`; in src/stores/signalr/signalr-store.ts:10 use
`@/stores/feature-flags/store`; and in
src/stores/feature-flags/__tests__/store.test.ts:3,21,30,37,43-45 use the
corresponding `@/stores/feature-flags/store`, `@/lib/storage`, `@/stores/auth/store`,
and `@/stores/security/store` specifiers consistently for imports, mocks, and mock
retrieval.

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/stores/signalr/__tests__/signalr-store.test.ts`:
- Around line 51-59: Add a test for connectChatHub using the mocked
featureFlagsStore to return false for Chat.System, then assert
connectToHubWithEventingUrl is not called and isChatHubConnected remains false.
Keep the existing enabled and handling-path coverage unchanged.

---

Nitpick comments:
In `@src/stores/feature-flags/__tests__/store.test.ts`:
- Around line 175-211: Update each useChatSystemStatus test to destructure
unmount from its renderHook result and call unmount after the assertions; in the
enabled/disabled test, unmount each separately rendered hook before or after
updating state while preserving the existing assertions.

In `@src/stores/feature-flags/store.ts`:
- Around line 8-9: Replace the relative imports and mock references with
configured `@/`* aliases: in src/stores/feature-flags/store.ts:8-9 use
`@/stores/auth/store` and `@/stores/security/store`; in
src/stores/signalr/signalr-store.ts:10 use `@/stores/feature-flags/store`; and in
src/stores/feature-flags/__tests__/store.test.ts:3,21,30,37,43-45 use the
corresponding `@/stores/feature-flags/store`, `@/lib/storage`, `@/stores/auth/store`,
and `@/stores/security/store` specifiers consistently for imports, mocks, and mock
retrieval.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3eb655bf-0955-4dd5-8766-2d1daba2445b

📥 Commits

Reviewing files that changed from the base of the PR and between 00dcaa3 and d5340c8.

📒 Files selected for processing (11)
  • src/app/(app)/chat.tsx
  • src/app/(app)/chatbot.tsx
  • src/app/chat/[channelId].tsx
  • src/app/chat/thread/[messageId].tsx
  • src/services/__tests__/app-reset.service.test.ts
  • src/services/app-reset.service.ts
  • src/stores/feature-flags/__tests__/store.test.ts
  • src/stores/feature-flags/store.ts
  • src/stores/signalr/__tests__/signalr-store.test.ts
  • src/stores/signalr/__tests__/zz-dbg.test.ts
  • src/stores/signalr/signalr-store.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/chat/[channelId].tsx
  • src/app/chat/thread/[messageId].tsx

Comment on lines +51 to +59
// Feature flags: default the chat flag to enabled so connectChatHub is not short-circuited.
jest.mock('../../feature-flags/store', () => ({
FeatureFlagKeys: { ChatSystem: 'Chat.System' },
featureFlagsStore: {
getState: jest.fn(() => ({
isEnabled: jest.fn(() => true),
})),
},
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 4 'connectChatHub|isEnabled|ChatSystem|connectToHubWithEventingUrl|isChatHubConnected' src/stores/signalr/__tests__/signalr-store.test.ts

Repository: Resgrid/Unit

Length of output: 2800


Add disabled Chat.System coverage for connectChatHub.

The current mock enables Chat.System for every test, and the returned matches only assert the enabled/handling paths. Add a case that sets featureFlagsStore.getState().isEnabled() to false for the default/hub flag and asserts that connectToHubWithEventingUrl() is not called and isChatHubConnected does not become true.

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

In `@src/stores/signalr/__tests__/signalr-store.test.ts` around lines 51 - 59, Add
a test for connectChatHub using the mocked featureFlagsStore to return false for
Chat.System, then assert connectToHubWithEventingUrl is not called and
isChatHubConnected remains false. Keep the existing enabled and handling-path
coverage unchanged.

Source: Coding guidelines

@ucswift

ucswift commented Aug 8, 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 c93a060 into master Aug 8, 2026
19 of 20 checks passed
Comment thread src/app/(app)/chatbot.tsx
const { t } = useTranslation();
const currentUserId = useAuthStore((s) => s.userId);
const chatStatus = useChatSystemStatus();
const isChatEnabled = chatStatus === 'enabled';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules low

Magic string vulnerability across src/app/(app)/chatbot.tsx and src/app/(app)/chat.tsx compares the chat system status against the raw string literal 'enabled', risking silent failures from typos. Define an enum such as enum ChatSystemStatus { Enabled = 'enabled', Disabled = 'disabled', Unknown = 'unknown' } and compare against ChatSystemStatus.Enabled to centralize domain knowledge and enforce compile-time checks.

Kody rule violation: Use enums instead of magic strings

Prompt for LLM

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

Line 29:

Magic string vulnerability across `src/app/(app)/chatbot.tsx` and `src/app/(app)/chat.tsx` compares the chat system status against the raw string literal 'enabled', risking silent failures from typos. Define an enum such as `enum ChatSystemStatus { Enabled = 'enabled', Disabled = 'disabled', Unknown = 'unknown' }` and compare against `ChatSystemStatus.Enabled` to centralize domain knowledge and enforce compile-time checks.

Talk to Kody by mentioning @kody

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

Comment on lines +39 to +40
const userId = useAuthStore.getState().userId;
const departmentId = securityStore.getState().rights?.DepartmentId;

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 state access in src/stores/feature-flags/store.ts re-fetches userId and departmentId exactly as getCurrentIdentityKey does at lines 25-26, risking implementation drift. Extract a helper such as getIdentityParts(): { userId: string | null; departmentId: string | null } or reuse getCurrentIdentityKey() to synchronize auth and security store access.

Kody rule violation: Extract duplicated logic into functions

Prompt for LLM

File src/stores/feature-flags/store.ts:

Line 39 to 40:

Duplicated state access in `src/stores/feature-flags/store.ts` re-fetches `userId` and `departmentId` exactly as `getCurrentIdentityKey` does at lines 25-26, risking implementation drift. Extract a helper such as `getIdentityParts(): { userId: string | null; departmentId: string | null }` or reuse `getCurrentIdentityKey()` to synchronize auth and security store access.

Talk to Kody by mentioning @kody

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

return { securityStore: mockSecurityStore };
});
jest.mock('../../feature-flags/store', () => ({
FeatureFlagKeys: { ChatSystem: 'Chat.System' },

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 mock constant in src/stores/signalr/__tests__/zz-dbg.test.ts duplicates the string literal 'Chat.System' for FeatureFlagKeys.ChatSystem, risking silent test drift and violating Rule [6]. Use const actual = jest.requireActual('../../feature-flags/store') to pull the real FeatureFlagKeys into the mock factory, then spread ...actual.FeatureFlagKeys while only overriding featureFlagsStore.

Kody rule violation: Centralize string constants

Prompt for LLM

File src/stores/signalr/__tests__/zz-dbg.test.ts:

Line 35:

Hardcoded mock constant in `src/stores/signalr/__tests__/zz-dbg.test.ts` duplicates the string literal 'Chat.System' for `FeatureFlagKeys.ChatSystem`, risking silent test drift and violating Rule [6]. Use `const actual = jest.requireActual('../../feature-flags/store')` to pull the real `FeatureFlagKeys` into the mock factory, then spread `...actual.FeatureFlagKeys` while only overriding `featureFlagsStore`.

Talk to Kody by mentioning @kody

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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants