From f3550f625350f9f7521827e51663cc2bfbe37efe Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Sun, 8 Nov 2026 19:15:38 -0800 Subject: [PATCH 1/2] RO-T101 IC Permissions Update --- .claude/skills/resgrid-domain/SKILL.md | 2 +- docs/architecture/omni-plan.md | 2 +- .../__tests__/command-store.test.ts | 56 +++++++++++++-- .../__tests__/command-support-store.test.ts | 3 +- .../src/incident-command/command-handlers.ts | 35 ++++++++- .../src/incident-command/command-store.ts | 6 +- .../incident-command/command-support-store.ts | 10 ++- .../__tests__/command-board-view.test.tsx | 72 ++++++++++++++++++- .../incident-command/__tests__/fixtures.ts | 7 +- .../__tests__/incident-command-lib.test.ts | 60 ++++++++++++++-- .../__tests__/incident-command-tab.test.tsx | 10 ++- .../components/command-board-view.tsx | 44 +++++++----- .../components/incident-command-tab.tsx | 8 ++- .../components/sheets/command-sheets.tsx | 30 ++++---- .../components/voice-section.tsx | 9 ++- .../hooks/use-command-permissions.ts | 41 ++++++++--- .../src/incident-command/lib/permissions.ts | 58 +++++++-------- .../navigation-and-preferences.test.ts | 11 ++- .../__tests__/storage-sync-screen.test.tsx | 2 +- packages/features/src/settings/lib/format.ts | 25 +++++++ .../settings/screens/storage-sync-screen.tsx | 4 +- packages/models/src/__tests__/models.test.ts | 5 ++ .../incidentCommand/incidentCommandEnums.ts | 32 +++++++-- .../incidentCommand/incidentCommandModels.ts | 3 +- .../v4/security/departmentRightsResultData.ts | 5 ++ tooling/oxlint/anti-slop-baseline.json | 10 +-- tsconfig.tsbuildinfo | 1 + 27 files changed, 437 insertions(+), 114 deletions(-) create mode 100644 tsconfig.tsbuildinfo diff --git a/.claude/skills/resgrid-domain/SKILL.md b/.claude/skills/resgrid-domain/SKILL.md index 8eef9d3c..e4ad05a7 100644 --- a/.claude/skills/resgrid-domain/SKILL.md +++ b/.claude/skills/resgrid-domain/SKILL.md @@ -23,7 +23,7 @@ Sources (verify there before relying on a detail): Core at `../Core` (`CLAUDE.md | Dispatch | Dispatcher CAD console: new/edit/close/pending/scheduled/archived calls, run cards, unit and personnel actions | Desktop/web, Electron | | BigBoard | Unattended wall display: widget grid (personnel, units with status timers, unit alerts, calls, map, weather) | TV / monitor | -App login gates: `CanLoginToDispatchApp`, `CanLoginToCommandApp` in rights (permissions 29/30, default allow). The app identifies itself with `X-Resgrid-Client` (`responder|unit|dispatch|bigboard|command|ic|…`) → server claim `client_app`; the server changes behaviour per app (e.g. BigBoard gets a redacted "safe shell" for protected departments). +App login gates: `CanLoginToDispatchApp`, `CanLoginToCommandApp` in rights (permissions 29/30, default allow). `CanLoginToCommandApp` is also the department's **run-command** permission (establish command; work boards where the member holds incident capabilities). **View Command Boards** (permission 125, rights `CanViewCommandBoards`; no row = follows 30) decides who reads every board; members on an incident (command staff, lane leads, dispatched directly or via group/role/a unit they crew, placed on the board) always read that board. Per board, `IncidentRoles/GetMyCapabilities` (bitmask on the envelope, not `Data`) is exactly what Core accepts: commander and establisher get all, ICS roles parts, everyone else read-only — gate every board control on it. The app identifies itself with `X-Resgrid-Client` (`responder|unit|dispatch|bigboard|command|ic|…`) → server claim `client_app`; the server changes behaviour per app (e.g. BigBoard gets a redacted "safe shell" for protected departments). ## Vocabulary diff --git a/docs/architecture/omni-plan.md b/docs/architecture/omni-plan.md index 0b371d6b..df6c31ff 100644 --- a/docs/architecture/omni-plan.md +++ b/docs/architecture/omni-plan.md @@ -534,7 +534,7 @@ LiveKit store from U (Android routing via InCallAudioModule, room-switch race fi | Units / personnel | R lists/cards/filters + D detail pages + U unit-type icon. | | Check-in / PAR | R check-in target + D sheet + U timer card. | | Video feeds | D (form, player) + U format utils. | -| Incident command | I canonical; tiers `view` (R/U), `assist` (D), `command` (I). | +| Incident command | I canonical; tiers `view` (R/U), `assist` (D), `command` (I). Assist and command gate every write on the server's per-board capabilities (`GetMyCapabilities`); IC treats the member's own board as fully theirs until they load. | | Settings | Section registry: R items (Bluetooth, server URL, login info, audio device, navigation app) + U unit selection + U/D sounds; per-variant sections. | | Navigation hand-off | R/U/I 10-app hand-off (`lib/navigation.ts` + `storage/navigation-app.ts`) for all variants. | | Maps | MapLibre plan §8.1 wrapper in `@resgrid/maps`; component baselines: pin marker I + U active state + D memo, map pins R, pin detail U, legend R, pickers R + D web, static map I (then server static maps), POI set R, layers sheet I, Unit location marker/full-screen/camera U, Dispatch legend/focus banner D. | diff --git a/packages/data/src/incident-command/__tests__/command-store.test.ts b/packages/data/src/incident-command/__tests__/command-store.test.ts index 767e0119..5f450343 100644 --- a/packages/data/src/incident-command/__tests__/command-store.test.ts +++ b/packages/data/src/incident-command/__tests__/command-store.test.ts @@ -1,5 +1,5 @@ -import { closeCallWith, closeCommand, establishCommand, getAdHocPersonnel, getAdHocUnits, getCommandBoard, getResourceIncidentView, getSyncBundle, saveObjective } from '@resgrid/api'; -import type { IncidentCommandBoard } from '@resgrid/models'; +import { assignResource, closeCallWith, closeCommand, establishCommand, getAdHocPersonnel, getAdHocUnits, getCommandBoard, getResourceIncidentView, getSyncBundle, moveResource, saveObjective } from '@resgrid/api'; +import { BaseV4Request, type IncidentCommandBoard, type ResourceAssignment } from '@resgrid/models'; import { defaultResetRegistry } from '@resgrid/offline'; import { createDataTestRuntime, type DataTestRuntime, flush, httpError, notFound, ok, persisted } from '../../__tests__/helpers'; @@ -11,6 +11,8 @@ jest.mock('@resgrid/api', () => ({ ...jest.requireActual('@resgrid/api'), establishCommand: jest.fn(), closeCommand: jest.fn(), + moveResource: jest.fn(), + assignResource: jest.fn(), closeCallWith: jest.fn(), getCommandBoard: jest.fn(), getAdHocUnits: jest.fn(), @@ -34,12 +36,16 @@ const board = (callId: number, extra: Partial = {}): Incid ...extra, }) as unknown as IncidentCommandBoard; +/** A v4 success envelope as the API functions resolve it. */ +const envelope = (Data: T): BaseV4Request & { Data: T } => ({ ...new BaseV4Request(), Status: 'success', Data }); + let runtime: DataTestRuntime; beforeEach(async () => { runtime = await createDataTestRuntime({ variant: 'ic' }); jest.mocked(getAdHocUnits).mockResolvedValue(ok([]) as never); jest.mocked(getAdHocPersonnel).mockResolvedValue(ok([]) as never); + for (const name of ['data.incident-command', 'data.resource-view']) await defaultResetRegistry.getStoreResets().get(name)?.('logout'); }); @@ -64,12 +70,14 @@ describe('incident command board (IC stores/command/*)', () => { jest.mocked(getCommandBoard).mockResolvedValue(ok(board(7)) as never); useCommandStore.setState({ boards: { '7': { callId: '7', board: board(7), adHocUnits: [], adHocPersonnel: [], isProvisional: false, lastRefreshed: 1 } } }); runtime.connectivity.setOnline(false); + const result = await useCommandStore.getState().dispatchCommandEvent(IC_EVENT.saveObjective, { callId: '7', resolveCommandId: true, body: { CallId: 7, Name: 'Search floor 2', ObjectiveType: 1 }, local: { collection: 'Objectives', idField: 'TacticalObjectiveId' }, }); + expect(useCommandStore.getState().boards['7'].board?.Objectives).toEqual([expect.objectContaining({ TacticalObjectiveId: `local-${result.id}`, Name: 'Search floor 2' })]); expect((await runtime.rows())[0]).toMatchObject({ orderKey: incidentOrderKey('7') }); @@ -106,11 +114,51 @@ describe('incident command board (IC stores/command/*)', () => { expect(jest.mocked(closeCommand).mock.invocationCallOrder[0]).toBeLessThan(jest.mocked(closeCallWith).mock.invocationCallOrder[0]); expect(closeCallWith).toHaveBeenCalledWith(expect.anything(), { callId: '7', type: 1, note: 'Done', sendNotification: true }); - useCommandStore.setState({ boards: { '8': { callId: '8', board: board(8), adHocUnits: [], adHocPersonnel: [], isProvisional: false, lastRefreshed: 1 } } }); - jest.mocked(closeCommand).mockRejectedValueOnce(httpError(400)); + useCommandStore.setState({ boards: { '8': { callId: '8', board: board(8), adHocUnits: [], adHocPersonnel: [], isProvisional: false, lastRefreshed: 1 } }, activeCallId: '8' }); + jest.mocked(closeCommand).mockRejectedValueOnce(httpError(403)); const refused = await useCommandStore.getState().endCommand('8', { closeCall: { type: 1 } }); expect(refused.command?.outcome).toBe('rejected'); expect(refused.call).toBeNull(); + // The command is still active on the server: the board stays. + expect(useCommandStore.getState().boards['8'].board?.Command.IncidentCommandId).toBe('ic-8'); + expect(useCommandStore.getState().activeCallId).toBe('8'); + }); + + it('moves a resource to a lane through MoveResource and back to the pool through AssignResource', async () => { + const row: ResourceAssignment = { + ResourceAssignmentId: 'a1', + IncidentCommandId: 'ic-7', + DepartmentId: 1, + CallId: 7, + CommandStructureNodeId: 'n1', + ResourceKind: 1, + ResourceId: 'u1', + AssignedByUserId: 'u0', + AssignedOn: '2026-11-08T22:00:00Z', + RequirementsWarning: false, + }; + + const withRow = board(7, { Assignments: [row] }); + jest.mocked(getCommandBoard).mockResolvedValue(envelope(withRow)); + jest.mocked(moveResource).mockResolvedValue(envelope(row)); + jest.mocked(assignResource).mockResolvedValue(envelope(row)); + useCommandStore.setState({ boards: { '7': { callId: '7', board: withRow, adHocUnits: [], adHocPersonnel: [], isProvisional: false, lastRefreshed: 1 } } }); + + const toLane = await useCommandStore.getState().dispatchCommandEvent(IC_EVENT.moveResource, { callId: '7', body: { ResourceAssignmentId: 'a1', TargetNodeId: 'n2' } }); + expect(toLane.outcome).toBe('sent'); + expect(moveResource).toHaveBeenCalledWith(expect.anything(), { ResourceAssignmentId: 'a1', TargetNodeId: 'n2' }); + + // Core's MoveResource refuses a blank target (400): the pool move re-saves the row with no lane. + const toPool = await useCommandStore.getState().dispatchCommandEvent(IC_EVENT.moveResource, { callId: '7', body: { ResourceAssignmentId: 'a1', TargetNodeId: '' } }); + expect(toPool.outcome).toBe('sent'); + expect(moveResource).toHaveBeenCalledTimes(1); + expect(assignResource).toHaveBeenCalledWith(expect.anything(), { ...row, CommandStructureNodeId: '' }); + + // A row on neither the board nor the server (released meanwhile) has nothing to move. + jest.mocked(getCommandBoard).mockResolvedValue(envelope(board(7))); + const gone = await useCommandStore.getState().dispatchCommandEvent(IC_EVENT.moveResource, { callId: '7', body: { ResourceAssignmentId: 'a9', TargetNodeId: '' } }); + expect(gone.outcome).toBe('sent'); + expect(assignResource).toHaveBeenCalledTimes(1); }); it('syncs every board from /Sync/Bundle and persists in the frozen command-storage shape', async () => { diff --git a/packages/data/src/incident-command/__tests__/command-support-store.test.ts b/packages/data/src/incident-command/__tests__/command-support-store.test.ts index da78d6eb..310876c3 100644 --- a/packages/data/src/incident-command/__tests__/command-support-store.test.ts +++ b/packages/data/src/incident-command/__tests__/command-support-store.test.ts @@ -39,7 +39,8 @@ beforeEach(async () => { describe('command support store', () => { it('reads the effective capability bitmask per incident (0 when the server sends none)', async () => { - jest.mocked(getMyIncidentCapabilities).mockResolvedValueOnce(ok({ Value: 9, Capabilities: ['ViewBoard', 'AssignResources'] }) as never); + // The bitmask is on the envelope, as Core sends it (no Data). + jest.mocked(getMyIncidentCapabilities).mockResolvedValueOnce({ Status: 'success', Value: 9, Capabilities: ['ViewBoard', 'AssignResources'] } as never); await useCommandSupportStore.getState().fetchCapabilities('7'); expect(getMyIncidentCapabilities).toHaveBeenCalledWith(expect.anything(), { callId: 7 }); expect(useCommandSupportStore.getState().capabilitiesByCall['7']).toBe(9); diff --git a/packages/data/src/incident-command/command-handlers.ts b/packages/data/src/incident-command/command-handlers.ts index fb006e1e..41b074f2 100644 --- a/packages/data/src/incident-command/command-handlers.ts +++ b/packages/data/src/incident-command/command-handlers.ts @@ -41,6 +41,7 @@ import type { IncidentTimer, MoveResourceInput, ResourceAssignment, + ResourceAssignmentResult, SetNeedStatusInput, TacticalObjective, UpdateCommandDetailsInput, @@ -63,15 +64,42 @@ class NoIncidentCommandError extends Error { /** The server IncidentCommandId for a call (writes queued before the command existed). Retried until the establish lands. */ const resolveIncidentCommandId = async (ctx: ApiContext, callId: string): Promise => { const known = useCommandStore.getState().boards[callId]?.board?.Command?.IncidentCommandId; + if (known && !known.startsWith('local-')) return known; const board = unwrapItem(await getCommandBoard(ctx, { callId })); const id = board?.Command?.IncidentCommandId; + if (!id) throw new NoIncidentCommandError(); + return id; }; const param = (payload: IcEventPayload, name: string): string => payload.params?.[name] ?? ''; +/** The board's row for an assignment, else the server's (the board may not have refreshed since the write was queued). */ +const findAssignment = async (ctx: ApiContext, callId: string, resourceAssignmentId: string): Promise => { + const known = useCommandStore.getState().boards[callId]?.board?.Assignments?.find((row) => row.ResourceAssignmentId === resourceAssignmentId); + + if (known) return known; + + const board = unwrapItem(await getCommandBoard(ctx, { callId })); + + return board?.Assignments?.find((row) => row.ResourceAssignmentId === resourceAssignmentId) ?? null; +}; + +/** + * Core's MoveResource only targets a lane (a blank TargetNodeId is a 400), so a move back to the unassigned pool + * re-saves the assignment through AssignResource, which upserts by id and keeps a blank node as the pool. An + * assignment no longer on the board has nothing to move — delivered, like MoveResource's own not_found. + */ +const sendMoveResource = async (ctx: ApiContext, callId: string, input: MoveResourceInput): Promise => { + if (input.TargetNodeId?.trim()) return moveResource(ctx, input); + + const assignment = await findAssignment(ctx, callId, input.ResourceAssignmentId); + + return assignment ? assignResource(ctx, { ...assignment, CommandStructureNodeId: '' }) : null; +}; + type Sender = (ctx: ApiContext, payload: IcEventPayload, body: Record) => Promise; /** One sender per IC event type (IC `offline-event-manager` replay table, on the typed registry). */ @@ -90,7 +118,7 @@ const SENDERS: Record = { [IC_EVENT.updateNode]: (ctx, _payload, body) => saveNode(ctx, body as unknown as CommandStructureNode), [IC_EVENT.deleteNode]: (ctx, payload) => deleteNode(ctx, { commandStructureNodeId: param(payload, 'commandStructureNodeId') }), [IC_EVENT.assignResource]: (ctx, _payload, body) => assignResource(ctx, body as unknown as ResourceAssignment), - [IC_EVENT.moveResource]: (ctx, _payload, body) => moveResource(ctx, body as unknown as MoveResourceInput), + [IC_EVENT.moveResource]: (ctx, payload, body) => sendMoveResource(ctx, payload.callId, body as unknown as MoveResourceInput), [IC_EVENT.releaseResource]: (ctx, payload) => releaseResource(ctx, { resourceAssignmentId: param(payload, 'resourceAssignmentId') }), [IC_EVENT.saveObjective]: (ctx, _payload, body) => saveObjective(ctx, body as unknown as TacticalObjective), [IC_EVENT.completeObjective]: (ctx, payload, body) => completeObjective(ctx, { tacticalObjectiveId: param(payload, 'tacticalObjectiveId') }, body as unknown as CompleteObjectiveInput), @@ -112,19 +140,24 @@ const createIcHandler = (type: IcEventType): OutboxHandler { const { local, body, callId } = event.payload; + if (!local) return; useCommandStore.getState().addLocalRow({ eventId: event.id, callId, collection: local.collection, row: { ...(body ?? {}), [local.idField]: `local-${event.id}`, __localEventId: event.id } }); }, send: async (event) => { const ctx = getDataApiContext(); const body = { ...(event.payload.body ?? {}) }; + if (event.payload.resolveCommandId) body.IncidentCommandId = await resolveIncidentCommandId(ctx, event.payload.callId); + return assertV4Success(await SENDERS[type](ctx, event.payload, body)); }, afterAck: async (event) => { const store = useCommandStore.getState(); store.removeLocalRow(event.id); + if (type === IC_EVENT.establishCommand) store.markBoardEstablished(event.payload.callId); + if (!NO_REFRESH.has(type) && store.boards[event.payload.callId]) await store.refreshBoard(event.payload.callId); }, rollback: (event) => useCommandStore.getState().removeLocalRow(event.id), diff --git a/packages/data/src/incident-command/command-store.ts b/packages/data/src/incident-command/command-store.ts index 513751ff..4513341a 100644 --- a/packages/data/src/incident-command/command-store.ts +++ b/packages/data/src/incident-command/command-store.ts @@ -176,13 +176,15 @@ export const useCommandStore = create()( if (incidentCommandId && !entry?.isProvisional) { command = await get().dispatchCommandEvent(IC_EVENT.closeCommand, { callId, params: { incidentCommandId } }); } + // A refused close leaves the command active on the server: the board stays, and the call is not closed (the + // server refuses to close a call whose command is still active). + if (command?.outcome === 'rejected') return { command, call: null }; const boards = { ...get().boards }; delete boards[callId]; const remaining = Object.keys(boards); set((state) => ({ boards, activeCallId: state.activeCallId === callId ? (remaining[0] ?? null) : state.activeCallId })); let call: WriteResult | null = null; - // Never after a refused command close: the server refuses to close a call whose command is still active. - if (options?.closeCall && command?.outcome !== 'rejected') { + if (options?.closeCall) { call = await get().dispatchCommandEvent( IC_EVENT.closeCall, { callId, body: { callId, type: options.closeCall.type, note: options.closeCall.notes ?? '', sendNotification: options.closeCall.sendNotification } }, diff --git a/packages/data/src/incident-command/command-support-store.ts b/packages/data/src/incident-command/command-support-store.ts index 423a7031..36a9f77e 100644 --- a/packages/data/src/incident-command/command-support-store.ts +++ b/packages/data/src/incident-command/command-support-store.ts @@ -16,7 +16,10 @@ type SupportSlice = 'capabilities' | 'definitions' | 'voiceChannels'; * the board; templates and voice channels are online conveniences (a blank board always works offline). */ interface CommandSupportData extends SliceMeta { - /** Effective `IncidentCapabilities` bitmask per incident (`IncidentRoles/GetMyCapabilities`); absent = not loaded. */ + /** + * Effective `IncidentCapabilities` bitmask per incident (`IncidentRoles/GetMyCapabilities`): exactly the writes the server + * accepts from this member there, plus `ViewBoard` when they may read it. Absent = not loaded. + */ capabilitiesByCall: Record; /** Department command templates (`Commands/GetAllCommands`). */ definitions: CommandResultData[]; @@ -57,8 +60,9 @@ export const useCommandSupportStore = create()((set, get) = fetchCapabilities: async (callId, options) => { await supportRunner.run( 'capabilities', - async () => unwrapItem(await getMyIncidentCapabilities(ctx(), { callId: toCallParam(callId) })), - (data, state) => ({ capabilitiesByCall: { ...state.capabilitiesByCall, [callId]: typeof data?.Value === 'number' ? data.Value : 0 } }), + // Core puts the bitmask on the envelope (`Value`), not in `Data`; a member who may do nothing gets 0. + async () => assertV4Success(await getMyIncidentCapabilities(ctx(), { callId: toCallParam(callId) })), + (result, state) => ({ capabilitiesByCall: { ...state.capabilitiesByCall, [callId]: typeof result?.Value === 'number' ? result.Value : 0 } }), { flightKey: `capabilities:${callId}`, trailing: options?.bypassCache === true } ); }, diff --git a/packages/features/src/incident-command/__tests__/command-board-view.test.tsx b/packages/features/src/incident-command/__tests__/command-board-view.test.tsx index 631ba6d9..c6167ebe 100644 --- a/packages/features/src/incident-command/__tests__/command-board-view.test.tsx +++ b/packages/features/src/incident-command/__tests__/command-board-view.test.tsx @@ -111,6 +111,16 @@ describe('CommandBoardView — command tier (IC)', () => { expect(onEnded).toHaveBeenCalled(); }); + it('stays on the board when the server refuses to end command', async () => { + mockEndCommand.mockResolvedValueOnce({ command: { outcome: 'rejected', id: 'c1' }, call: null }); + const onEnded = jest.fn(); + await render(); + await fireEvent.press(screen.getByTestId('command-end')); + await act(async () => fireEvent.press(screen.getByTestId('end-command-dialog-submit', hidden))); + expect(lastToast()).toMatchObject({ type: 'error', message: 'incident_command.end_command_failed' }); + expect(onEnded).not.toHaveBeenCalled(); + }); + it('transfers command to another member (online) and explains a failure', async () => { const transferCommand = jest.fn(async () => undefined); useCommandSupportStore.setState({ transferCommand }); @@ -205,12 +215,72 @@ describe('CommandBoardView — command tier (IC)', () => { }); }); +describe('CommandBoardView — command tier (IC), another member’s board', () => { + // Every board write control; none may show to a member the server would refuse (no 403s from the board). + const writeControls = [ + 'structure-add-lane', + 'lane-assign-n1', + 'command-add-to-incident', + 'command-add-adhoc', + 'command-details', + 'command-transfer', + 'command-end', + 'command-objectives-add', + 'command-needs-add', + 'command-timers-add', + 'command-roles-add', + 'command-notes-add', + 'command-voice-add', + ]; + + beforeEach(() => { + // Signed in as someone who neither commands nor established this incident (makeBoard: u1 commands). + seedBoard('7', makeBoard(), {}, 'u9'); + }); + + it('shows no write control before the capabilities load', async () => { + await render(); + expect(mockFetchCapabilities).toHaveBeenCalledWith('7', { bypassCache: true }); + expect(screen.getByTestId('command-board-view')).toBeTruthy(); + + for (const id of writeControls) expect(screen.queryByTestId(id)).toBeNull(); + }); + + it('stays read-only when the server says the member may only read the board', async () => { + useCommandSupportStore.setState({ capabilitiesByCall: { '7': IncidentCapabilities.ViewBoard } }); + await render(); + + for (const id of writeControls) expect(screen.queryByTestId(id)).toBeNull(); + // Moving resources needs the assign capability: tapping a resource selects nothing. + await fireEvent.press(screen.getByTestId('lane-resource-a1')); + expect(screen.queryByTestId('structure-move-banner')).toBeNull(); + }); + + it('offers what an ICS role grants and nothing more (Safety Officer: objectives, needs, internal notes)', async () => { + useCommandSupportStore.setState({ + capabilitiesByCall: { '7': IncidentCapabilities.ViewBoard | IncidentCapabilities.ManageObjectives | IncidentCapabilities.ManageAnnotations | IncidentCapabilities.ManageNotes }, + }); + await render(); + expect(screen.getByTestId('command-objectives-add')).toBeTruthy(); + expect(screen.getByTestId('command-needs-add')).toBeTruthy(); + expect(screen.getByTestId('command-notes-add')).toBeTruthy(); + + for (const id of ['structure-add-lane', 'command-add-to-incident', 'command-add-adhoc', 'command-end', 'command-roles-add', 'command-timers-add']) { + expect(screen.queryByTestId(id)).toBeNull(); + } + + await fireEvent.press(screen.getByTestId('command-notes-add')); + // Public notes need ManagePublicInformation. + expect(screen.queryByTestId('note-sheet-visibility', hidden)).toBeNull(); + }); +}); + describe('CommandBoardView — assist tier (Dispatch)', () => { beforeEach(() => activateVariant('dispatch')); it('reads the member capabilities and stays read-only until they arrive', async () => { await render(); - expect(mockFetchCapabilities).toHaveBeenCalledWith('7'); + expect(mockFetchCapabilities).toHaveBeenCalledWith('7', { bypassCache: true }); expect(screen.getByTestId('command-board-view')).toBeTruthy(); expect(screen.queryByTestId('structure-add-lane')).toBeNull(); expect(screen.queryByTestId('lane-assign-n1')).toBeNull(); diff --git a/packages/features/src/incident-command/__tests__/fixtures.ts b/packages/features/src/incident-command/__tests__/fixtures.ts index 847c5645..2351855e 100644 --- a/packages/features/src/incident-command/__tests__/fixtures.ts +++ b/packages/features/src/incident-command/__tests__/fixtures.ts @@ -1,4 +1,5 @@ // Board fixtures for the incident-command tests (not a test file). +import { useAuthStore } from '@resgrid/auth'; import { type CommandBoardState, useCommandStore, useCommandSupportStore, usePersonnelStore, useUnitsStore } from '@resgrid/data'; import { CommandNodeType, @@ -87,7 +88,11 @@ export const mockStartCommand = jest.fn(); export const mockFetchCapabilities = jest.fn(); /** Puts one board in the command store with every network action stubbed. */ -export function seedBoard(callId = '7', board: IncidentCommandBoard | null = makeBoard(), overrides: Partial = {}): void { +/** The board's commander (makeBoard: u1). Core gives them every capability, so the IC tier can work the board. */ +export const COMMANDER_USER_ID = 'u1'; + +export function seedBoard(callId = '7', board: IncidentCommandBoard | null = makeBoard(), overrides: Partial = {}, userId: string | null = COMMANDER_USER_ID): void { + useAuthStore.setState({ userId }); mockDispatch.mockResolvedValue({ outcome: 'sent', id: 'e1' }); mockRefreshBoard.mockResolvedValue(undefined); mockFetchTimeline.mockResolvedValue(undefined); diff --git a/packages/features/src/incident-command/__tests__/incident-command-lib.test.ts b/packages/features/src/incident-command/__tests__/incident-command-lib.test.ts index 851c6520..792b7808 100644 --- a/packages/features/src/incident-command/__tests__/incident-command-lib.test.ts +++ b/packages/features/src/incident-command/__tests__/incident-command-lib.test.ts @@ -111,7 +111,7 @@ describe('board rules', () => { describe('permissions by tier', () => { it('view and none never write or show the board', () => { for (const tier of ['none', 'view'] as const) { - expect(resolveCommandPermissions({ tier, capabilities: IncidentCapabilities.All, isClosed: false, canCreateCalls: true })).toMatchObject({ + expect(resolveCommandPermissions({ tier, capabilities: IncidentCapabilities.All, isClosed: false, canEstablish: true })).toMatchObject({ canViewBoard: false, canEstablish: false, canManageCommand: false, @@ -120,28 +120,74 @@ describe('permissions by tier', () => { } }); - it('command can do everything until the command is closed', () => { - expect(resolveCommandPermissions({ tier: 'command', isClosed: false, canCreateCalls: false })).toMatchObject({ + it('command follows the server capabilities, so another commander’s board is read-only', () => { + // The member may run command (they establish their own), but this board belongs to someone else. + expect(resolveCommandPermissions({ tier: 'command', capabilities: IncidentCapabilities.ViewBoard, isClosed: false, canEstablish: true })).toMatchObject({ canViewBoard: true, canEstablish: true, + canManageCommand: false, + canManageStructure: false, + canAssignResources: false, + canManageObjectives: false, + canManageNeeds: false, + canAddNotes: false, + canManageChannels: false, + }); + }); + + it('command treats the member’s own board as theirs until the capabilities load (offline too), and nothing else', () => { + expect(resolveCommandPermissions({ tier: 'command', isClosed: false, canEstablish: true, isOwnBoard: true })).toMatchObject({ canManageCommand: true, canManageStructure: true, canManageTimers: true, + canAddNotes: true, + canAddPublicNotes: true, + }); + expect(resolveCommandPermissions({ tier: 'command', isClosed: false, canEstablish: true })).toMatchObject({ canManageCommand: false, canAssignResources: false }); + // Loaded capabilities win over the fallback (command transferred away). + expect(resolveCommandPermissions({ tier: 'command', capabilities: IncidentCapabilities.ViewBoard, isClosed: false, canEstablish: true, isOwnBoard: true })).toMatchObject({ + canManageCommand: false, + }); + expect(resolveCommandPermissions({ tier: 'command', isClosed: true, canEstablish: true, isOwnBoard: true })).toMatchObject({ canViewBoard: true, canManageCommand: false, canAssignResources: false }); + }); + + it('each control maps to the capability Core checks for that write', () => { + const safety = resolveCommandPermissions({ + tier: 'command', + capabilities: IncidentCapabilities.ViewBoard | IncidentCapabilities.ManageObjectives | IncidentCapabilities.ManageNotes, + isClosed: false, + canEstablish: true, }); - expect(resolveCommandPermissions({ tier: 'command', isClosed: true, canCreateCalls: false })).toMatchObject({ canViewBoard: true, canManageCommand: false, canAssignResources: false }); + + // Needs are SaveNeed/SetNeedStatus (ManageObjectives); notes are AddNote (ManageNotes); public notes add ManagePublicInformation. + expect(safety).toMatchObject({ canManageNeeds: true, canAddNotes: true, canAddPublicNotes: false, canManageResources: false }); + + const logistics = resolveCommandPermissions({ + tier: 'command', + capabilities: IncidentCapabilities.ViewBoard | IncidentCapabilities.ManageResources | IncidentCapabilities.ManageChannels, + isClosed: false, + canEstablish: true, + }); + + expect(logistics).toMatchObject({ canManageResources: true, canManageChannels: true, canManageNeeds: false, canAddNotes: false }); }); it('assist follows the server capabilities and is read-only until they load', () => { - expect(resolveCommandPermissions({ tier: 'assist', isClosed: false, canCreateCalls: true })).toMatchObject({ canViewBoard: true, canEstablish: true, canManageCommand: false, canAssignResources: false }); + expect(resolveCommandPermissions({ tier: 'assist', isClosed: false, canEstablish: true, isOwnBoard: true })).toMatchObject({ + canViewBoard: true, + canEstablish: true, + canManageCommand: false, + canAssignResources: false, + }); const ops = resolveCommandPermissions({ tier: 'assist', capabilities: IncidentCapabilities.ViewBoard | IncidentCapabilities.AssignResources | IncidentCapabilities.ManageObjectives, isClosed: false, - canCreateCalls: false, + canEstablish: false, }); - expect(ops).toMatchObject({ canEstablish: false, canAssignResources: true, canManageObjectives: true, canManageStructure: false, canManageCommand: false, canAddNotes: true }); + expect(ops).toMatchObject({ canEstablish: false, canAssignResources: true, canManageObjectives: true, canManageStructure: false, canManageCommand: false, canAddNotes: false }); }); }); diff --git a/packages/features/src/incident-command/__tests__/incident-command-tab.test.tsx b/packages/features/src/incident-command/__tests__/incident-command-tab.test.tsx index 5ddafde3..df972d3c 100644 --- a/packages/features/src/incident-command/__tests__/incident-command-tab.test.tsx +++ b/packages/features/src/incident-command/__tests__/incident-command-tab.test.tsx @@ -2,7 +2,7 @@ import './test-setup'; import { securityStore } from '@resgrid/auth'; import { useCommandStore, useResourceViewStore } from '@resgrid/data'; -import type { DepartmentRightsResultData, ResourceIncidentView } from '@resgrid/models'; +import { DepartmentRightsResultData, type ResourceIncidentView } from '@resgrid/models'; import { IncidentCommandStatus } from '@resgrid/models'; import { useToastStore } from '@resgrid/ui'; import { act, fireEvent, render, screen } from '@testing-library/react-native'; @@ -192,4 +192,12 @@ describe('command tier (IC)', () => { expect(mockStartCommand).toHaveBeenCalledWith('7', null); expect(router.push).toHaveBeenCalledWith('/command'); }); + + it('hides establish when the department does not let the member run command (Command App Login)', async () => { + securityStore.setState({ rights: Object.assign(new DepartmentRightsResultData(), { CanCreateCalls: true, CanLoginToCommandApp: false }), isLoaded: true }); + seedBoard('8', makeBoard()); + await render(); + expect(screen.getByTestId('incident-command-tab-empty')).toBeTruthy(); + expect(screen.queryByTestId('incident-command-tab-establish')).toBeNull(); + }); }); diff --git a/packages/features/src/incident-command/components/command-board-view.tsx b/packages/features/src/incident-command/components/command-board-view.tsx index 89cfe141..25f55f02 100644 --- a/packages/features/src/incident-command/components/command-board-view.tsx +++ b/packages/features/src/incident-command/components/command-board-view.tsx @@ -210,7 +210,9 @@ export const CommandBoardView: React.FC = ({ callId, tier reportWrite(result.call, { sent: t('call_detail.close_call_success'), queued: t('incident_command.close_call_queued'), rejected: t('incident_command.close_call_failed') }); if (result.call?.outcome === 'sent') void useCallsStore.getState().fetchCalls({ bypassCache: true }); - onEnded?.(); + + // A refused close keeps the board (the command is still active on the server). + if (result.command?.outcome !== 'rejected') onEnded?.(); } catch { toast.error(t('incident_command.write_failed')); } @@ -221,6 +223,8 @@ export const CommandBoardView: React.FC = ({ callId, tier if (!entry) return null; const command = board?.Command ?? null; + // The move needs the assign capability; a confirmation left open after losing it never shows. + const visibleMoveConflict = permissions.canAssignResources ? moveConflict : null; const commanderName = command?.CurrentCommanderUserId ? roster.personName(command.CurrentCommanderUserId) : null; const title = call?.Number ? t('incident_command.call_number', { number: call.Number }) : t('incident_command.call_number', { number: callId }); const subtitle = command?.Name || (call ? callDisplayName(call, t('data_protection.protected_value')) : '') || null; @@ -282,7 +286,7 @@ export const CommandBoardView: React.FC = ({ callId, tier setSheet('objective')} onOpen={setOpenObjective} /> - setSheet('need')} onOpen={setOpenNeed} /> + setSheet('need')} onOpen={setOpenNeed} /> = ({ callId, tier onEnd={permissions.canManageCommand ? () => setSheet('end') : undefined} extraActions={headerActions} /> - {moveConflict ? ( + {visibleMoveConflict ? ( - {t('incident_command.move_conflict', { resource: moveConflict.name, from: moveConflict.from, to: moveConflict.to })} + {t('incident_command.move_conflict', { resource: visibleMoveConflict.name, from: visibleMoveConflict.from, to: visibleMoveConflict.to })} setMoveConflict(null)} testID="command-move-conflict-cancel" /> { - const conflict = moveConflict; + const conflict = visibleMoveConflict; setMoveConflict(null); void run(moveResourceEvent(callId, conflict.assignmentId, conflict.targetNodeId)); }} @@ -362,7 +366,7 @@ export const CommandBoardView: React.FC = ({ callId, tier lane.node)} @@ -384,10 +388,13 @@ export const CommandBoardView: React.FC = ({ callId, tier { success: saved } ); }} - onDelete={editLane ? (disposition) => void deleteLane(editLane, disposition) : undefined} + onDelete={ + // Deleting a lane that holds resources moves or releases them, which needs the assign capability too. + editLane && (permissions.canAssignResources || laneAssignments(assignments, editLane.CommandStructureNodeId).length === 0) ? (disposition) => void deleteLane(editLane, disposition) : undefined + } /> = ({ callId, tier }} /> { setSheet(null); @@ -434,7 +441,7 @@ export const CommandBoardView: React.FC = ({ callId, tier } /> { setSheet(null); @@ -455,7 +462,7 @@ export const CommandBoardView: React.FC = ({ callId, tier }} /> { setSheet(null); @@ -463,7 +470,7 @@ export const CommandBoardView: React.FC = ({ callId, tier }} /> setOpenNeed(null)} onSave={(need, status, quantity, note) => { setOpenNeed(null); @@ -471,7 +478,7 @@ export const CommandBoardView: React.FC = ({ callId, tier }} /> { setSheet(null); @@ -479,15 +486,16 @@ export const CommandBoardView: React.FC = ({ callId, tier }} /> { setSheet(null); void run(addNoteEvent(callId, body, visibility, noteTitle), { success: saved }); }} /> { @@ -496,7 +504,7 @@ export const CommandBoardView: React.FC = ({ callId, tier }} /> = ({ callId, tier }} /> = ({ callId, tier void run(commandDetailsEvent(callId, input), { success: saved }); }} /> - void confirmEnd(closeCall)} /> + void confirmEnd(closeCall)} /> {assistantFlag ? : null} ); diff --git a/packages/features/src/incident-command/components/incident-command-tab.tsx b/packages/features/src/incident-command/components/incident-command-tab.tsx index cf7faf1d..611e98dc 100644 --- a/packages/features/src/incident-command/components/incident-command-tab.tsx +++ b/packages/features/src/incident-command/components/incident-command-tab.tsx @@ -94,7 +94,7 @@ const AssistTab: React.FC<{ callId: string; fullScreen: boolean }> = ({ callId, <> setIsStartOpen(true)} /> setIsStartOpen(false)} onStart={(definitionId) => void startCommand(callId, definitionId).then( @@ -128,6 +128,8 @@ const AssistTab: React.FC<{ callId: string; fullScreen: boolean }> = ({ callId, const CommandTab: React.FC<{ callId: string }> = ({ callId }) => { const { t } = useTranslation(); const toast = useToast(); + // Establishing needs the department's run-command permission (Command App Login), the same check Core applies. + const { canEstablish } = useCommandPermissions(callId, { tier: 'command' }); const entry = useCommandStore((state) => state.boards[callId]); const summary = useCommandStore((state) => state.summaries.find((candidate) => String(candidate.CallId) === callId && candidate.Status === IncidentCommandStatus.Active)); const fetchIncidents = useCommandStore((state) => state.fetchIncidents); @@ -153,7 +155,7 @@ const CommandTab: React.FC<{ callId: string }> = ({ callId }) => { return ( <> setIsStartOpen(true)} extra={ summary ? ( @@ -164,7 +166,7 @@ const CommandTab: React.FC<{ callId: string }> = ({ callId }) => { } /> setIsStartOpen(false)} onStart={(definitionId) => void useCommandStore diff --git a/packages/features/src/incident-command/components/sheets/command-sheets.tsx b/packages/features/src/incident-command/components/sheets/command-sheets.tsx index 7f0af7f2..61a31658 100644 --- a/packages/features/src/incident-command/components/sheets/command-sheets.tsx +++ b/packages/features/src/incident-command/components/sheets/command-sheets.tsx @@ -59,10 +59,12 @@ export interface NoteSheetProps { isOpen: boolean; onClose: () => void; onSave: (body: string, visibility: number, title: string) => void; + /** Public visibility is offered only to members who may publish incident information (the server refuses it otherwise). */ + allowPublic?: boolean; } /** An operational status note — internal, or public (it may reach the public incident feed). */ -export const NoteSheet: React.FC = ({ isOpen, onClose, onSave }) => { +export const NoteSheet: React.FC = ({ isOpen, onClose, onSave, allowPublic = true }) => { const { t } = useTranslation(); const [title, setTitle] = useState(''); const [body, setBody] = useState(''); @@ -88,17 +90,21 @@ export const NoteSheet: React.FC = ({ isOpen, onClose, onSave }) > - - + {allowPublic ? ( + <> + + + + ) : null} ); }; diff --git a/packages/features/src/incident-command/components/voice-section.tsx b/packages/features/src/incident-command/components/voice-section.tsx index 692198e8..385f9430 100644 --- a/packages/features/src/incident-command/components/voice-section.tsx +++ b/packages/features/src/incident-command/components/voice-section.tsx @@ -38,6 +38,13 @@ export const VoiceSection: React.FC = ({ callId, canManage, t if (isVoiceEnabled) void fetchVoiceChannels(callId); }, [callId, fetchVoiceChannels, isVoiceEnabled]); + // The create panel never outlives the right to create (a role removed, command transferred or ended). + useEffect(() => { + if (!canManage) setIsAdding(false); + }, [canManage]); + + const showCreate = canManage ? isAdding : false; + const join = useCallback( async (channel: IncidentVoiceChannel) => { let room = rooms.find((candidate) => candidate.Id === channel.DepartmentVoiceChannelId); @@ -91,7 +98,7 @@ export const VoiceSection: React.FC = ({ callId, canManage, t action={canManage ? setIsAdding((open) => !open)} testID={`${testID}-add`} /> : undefined} testID={testID} > - {isAdding ? ( + {showCreate ? ( {[t('incident_command.voice_preset_tactical'), t('incident_command.voice_preset_command')].map((preset, index) => ( diff --git a/packages/features/src/incident-command/hooks/use-command-permissions.ts b/packages/features/src/incident-command/hooks/use-command-permissions.ts index b1f86864..a9b8c251 100644 --- a/packages/features/src/incident-command/hooks/use-command-permissions.ts +++ b/packages/features/src/incident-command/hooks/use-command-permissions.ts @@ -1,6 +1,6 @@ -import { useSecurity } from '@resgrid/auth'; +import { useAuthStore, useSecurity } from '@resgrid/auth'; import { useCapability } from '@resgrid/config'; -import { useCommandSupportStore } from '@resgrid/data'; +import { useCommandStore, useCommandSupportStore } from '@resgrid/data'; import { useEffect, useMemo } from 'react'; import { type CommandPermissions, type IncidentCommandTier, resolveCommandPermissions } from '../lib/permissions'; @@ -8,23 +8,48 @@ import { type CommandPermissions, type IncidentCommandTier, resolveCommandPermis /** The variant's tier, unless a caller (tests, an embedded panel) overrides it. */ export function useIncidentCommandTier(override?: IncidentCommandTier): IncidentCommandTier { const tier = useCapability('incidentCommand'); + return override ?? tier; } +const sameUser = (a: string | null | undefined, b: string | null | undefined): boolean => !!a && !!b && a.toLowerCase() === b.toLowerCase(); + /** - * Board permissions for this member on one incident (see `resolveCommandPermissions`). In the assist tier the server's - * effective capabilities are read (and re-read when the incident changes); other tiers never call the endpoint. + * Board permissions for this member on one incident (see `resolveCommandPermissions`). In the command and assist tiers the + * server's effective capabilities are read, and re-read whenever the command or its role assignments change (a transfer + * or a new ICS role changes what the member may do); other tiers never call the endpoint. */ export function useCommandPermissions(callId: string | null | undefined, options: { tier?: IncidentCommandTier; isClosed?: boolean } = {}): CommandPermissions { const tier = useIncidentCommandTier(options.tier); - const { canUserCreateCalls } = useSecurity(); + const { canUserCreateCalls, rights } = useSecurity(); + const userId = useAuthStore((state) => state.userId); const capabilities = useCommandSupportStore((state) => (callId ? state.capabilitiesByCall[callId] : undefined)); const fetchCapabilities = useCommandSupportStore((state) => state.fetchCapabilities); + const entry = useCommandStore((state) => (callId ? state.boards[callId] : undefined)); const isClosed = options.isClosed === true; + const command = entry?.board?.Command; + const fetchesCapabilities = tier === 'assist' || tier === 'command'; + + // Who commands and who holds which role decide the capabilities: refetch when either changes. + const capabilityKey = useMemo(() => { + const roles = (entry?.board?.Roles ?? []) + .filter((role) => !role.RemovedOn) + .map((role) => `${role.UserId}:${role.RoleType}`) + .sort() + .join(','); + + return `${command?.IncidentCommandId ?? ''}|${command?.CurrentCommanderUserId ?? ''}|${roles}`; + }, [command?.CurrentCommanderUserId, command?.IncidentCommandId, entry?.board?.Roles]); useEffect(() => { - if (tier === 'assist' && callId) void fetchCapabilities(callId); - }, [callId, fetchCapabilities, tier]); + if (fetchesCapabilities && callId) void fetchCapabilities(callId, { bypassCache: true }); + }, [callId, capabilityKey, fetchCapabilities, fetchesCapabilities]); + + // Core gives every capability to the commander and whoever established command; a board started on this device is + // the member's own until the server has it. Used only until the capabilities load, and offline. + const isOwnBoard = !!entry?.isProvisional || sameUser(command?.CurrentCommanderUserId, userId) || sameUser(command?.EstablishedByUserId, userId); + // IC establishes under the department's run-command permission (the IC sign-in gate); Dispatch under CanCreateCalls. + const canEstablish = tier === 'command' ? rights?.CanLoginToCommandApp !== false : canUserCreateCalls; - return useMemo(() => resolveCommandPermissions({ tier, capabilities, isClosed, canCreateCalls: canUserCreateCalls }), [canUserCreateCalls, capabilities, isClosed, tier]); + return useMemo(() => resolveCommandPermissions({ tier, capabilities, isClosed, canEstablish, isOwnBoard }), [canEstablish, capabilities, isClosed, isOwnBoard, tier]); } diff --git a/packages/features/src/incident-command/lib/permissions.ts b/packages/features/src/incident-command/lib/permissions.ts index cf85fb65..57d4dc81 100644 --- a/packages/features/src/incident-command/lib/permissions.ts +++ b/packages/features/src/incident-command/lib/permissions.ts @@ -7,10 +7,13 @@ export type IncidentCommandTier = Capabilities['incidentCommand']; /** * What the signed-in member may do on a command board. One board for every app, gated here (plan §7.10): * - `none` (BigBoard) and `view` (Responder/Unit): no board writes; R/U see their own slice (resource view). - * - `assist` (Dispatch): the server's effective `IncidentCapabilities` for this member and incident decide each action - * (Dispatch `command-board-view`); until they load, the board is read-only. Establishing needs `CanCreateCalls`. - * - `command` (IC): everything — command-app access is already enforced at sign-in (`CanLoginToCommandApp`), the board - * must work offline where capabilities cannot be fetched, and the server refuses anything a role may not do. + * - `assist` (Dispatch) and `command` (IC): the server's effective `IncidentCapabilities` for this member and incident + * (`IncidentRoles/GetMyCapabilities`) decide each action — the same checks Core applies to each write, so no control + * is offered that the server would refuse. Core gives every capability to the commander and to whoever established + * command; ICS roles and command assist give parts; anyone else reads the board without changing it. + * - Until the capabilities load (or offline), the IC tier treats a board the member commands, established, or started on + * this device as fully theirs, and every other board as read-only; Dispatch is read-only. + * Establishing needs the department's run-command permission (IC, `CanLoginToCommandApp`) or `CanCreateCalls` (Dispatch). * A closed command is read-only in every tier. */ export interface CommandPermissions { @@ -23,13 +26,18 @@ export interface CommandPermissions { canManageRoles: boolean; canManageStructure: boolean; canAssignResources: boolean; - /** Ad-hoc (non-Resgrid) units and personnel, needs. */ + /** Ad-hoc (non-Resgrid) units and personnel. */ canManageResources: boolean; + /** Incident needs and their status (Core: ManageObjectives). */ + canManageNeeds: boolean; canManageObjectives: boolean; canManageTimers: boolean; canManageAccountability: boolean; canManageChannels: boolean; + /** Add incident notes (Core: ManageNotes). */ canAddNotes: boolean; + /** Choose Public visibility for a note (Core: ManagePublicInformation). */ + canAddPublicNotes: boolean; } export const NO_COMMAND_PERMISSIONS: CommandPermissions = { @@ -40,11 +48,13 @@ export const NO_COMMAND_PERMISSIONS: CommandPermissions = { canManageStructure: false, canAssignResources: false, canManageResources: false, + canManageNeeds: false, canManageObjectives: false, canManageTimers: false, canManageAccountability: false, canManageChannels: false, canAddNotes: false, + canAddPublicNotes: false, }; export interface PermissionInput { @@ -52,44 +62,34 @@ export interface PermissionInput { /** Server bitmask for this member and incident; undefined while unknown. */ capabilities?: number; isClosed: boolean; - /** Rights DTO `CanCreateCalls` (assist tier establish). */ - canCreateCalls: boolean; + /** May establish command: rights `CanLoginToCommandApp` (command tier) or `CanCreateCalls` (assist tier). */ + canEstablish: boolean; + /** + * The member commands this board, established it, or started it on this device (provisional): Core gives them every + * capability, so the IC tier can work it before the capabilities load and offline. + */ + isOwnBoard?: boolean; } -export function resolveCommandPermissions({ tier, capabilities, isClosed, canCreateCalls }: PermissionInput): CommandPermissions { +export function resolveCommandPermissions({ tier, capabilities, isClosed, canEstablish, isOwnBoard = false }: PermissionInput): CommandPermissions { if (tier === 'none' || tier === 'view') return NO_COMMAND_PERMISSIONS; - if (tier === 'command') { - const write = !isClosed; - return { - canViewBoard: true, - canEstablish: true, - canManageCommand: write, - canManageRoles: write, - canManageStructure: write, - canAssignResources: write, - canManageResources: write, - canManageObjectives: write, - canManageTimers: write, - canManageAccountability: write, - canManageChannels: write, - canAddNotes: write, - }; - } - // assist - const value = typeof capabilities === 'number' ? capabilities : IncidentCapabilities.None; + const known = typeof capabilities === 'number'; + const value = known ? capabilities : tier === 'command' && isOwnBoard ? IncidentCapabilities.All : IncidentCapabilities.None; const can = (flag: IncidentCapabilities) => !isClosed && hasIncidentCapability(value, flag); return { canViewBoard: true, - canEstablish: canCreateCalls, + canEstablish, canManageCommand: can(IncidentCapabilities.ManageCommand), canManageRoles: can(IncidentCapabilities.ManageCommand), canManageStructure: can(IncidentCapabilities.ManageStructure), canAssignResources: can(IncidentCapabilities.AssignResources), canManageResources: can(IncidentCapabilities.ManageResources), + canManageNeeds: can(IncidentCapabilities.ManageObjectives), canManageObjectives: can(IncidentCapabilities.ManageObjectives), canManageTimers: can(IncidentCapabilities.ManageTimers), canManageAccountability: can(IncidentCapabilities.ManageAccountability), canManageChannels: can(IncidentCapabilities.ManageChannels), - canAddNotes: can(IncidentCapabilities.ManageCommand) || can(IncidentCapabilities.ManageObjectives), + canAddNotes: can(IncidentCapabilities.ManageNotes), + canAddPublicNotes: can(IncidentCapabilities.ManageNotes) && can(IncidentCapabilities.ManagePublicInformation), }; } diff --git a/packages/features/src/settings/__tests__/navigation-and-preferences.test.ts b/packages/features/src/settings/__tests__/navigation-and-preferences.test.ts index 0c09a434..89e8d1e7 100644 --- a/packages/features/src/settings/__tests__/navigation-and-preferences.test.ts +++ b/packages/features/src/settings/__tests__/navigation-and-preferences.test.ts @@ -5,7 +5,7 @@ import { createMemoryKvBackend, DurableKv } from '@resgrid/offline'; import { Linking } from 'react-native'; import { setDevicePreferencesKv } from '../../auth-ui/lib/device-kv'; -import { ageParts, formatBytes } from '../lib/format'; +import { ageParts, formatBytes, outboxErrorLabel } from '../lib/format'; import { getNavigationApp, getNavigationAppOptions, normalizeNavigationApp, setNavigationApp } from '../lib/navigation-app'; import { buildNavigationAttempts, NAVIGATION_ORIGIN_MAX_AGE_MS, openMapsWithDirections, openNavigationTarget, toNavigationCoordinates } from '../lib/navigation-handoff'; import { getKeepAlivePreference, getLocationPreferences, getThemeMode, subscribePreferences } from '../lib/preferences'; @@ -168,4 +168,13 @@ describe('format', () => { expect(ageParts(90 * 60_000)).toEqual({ key: 'hours', count: 1 }); expect(ageParts(72 * 3_600_000)).toEqual({ key: 'days', count: 3 }); }); + + it('labels a queued change error by HTTP status, never by the generic axios code', () => { + expect(outboxErrorLabel(undefined)).toBeNull(); + expect(outboxErrorLabel({ status: 403, code: 'ERR_BAD_REQUEST', message: 'Request failed with status code 403' })).toBe('HTTP 403'); + expect(outboxErrorLabel({ status: 502, code: 'ERR_BAD_RESPONSE', message: 'Bad gateway' })).toBe('HTTP 502'); + expect(outboxErrorLabel({ status: 422, code: 'validation_failed', message: 'Note too long' })).toBe('HTTP 422 · validation_failed'); + expect(outboxErrorLabel({ code: 'payload_unreadable', message: 'The queued payload could not be decrypted.' })).toBe('payload_unreadable'); + expect(outboxErrorLabel({ message: 'No active incident command for the call yet.' })).toBe('No active incident command for the call yet.'); + }); }); diff --git a/packages/features/src/settings/__tests__/storage-sync-screen.test.tsx b/packages/features/src/settings/__tests__/storage-sync-screen.test.tsx index 2fd662d6..0464b6f7 100644 --- a/packages/features/src/settings/__tests__/storage-sync-screen.test.tsx +++ b/packages/features/src/settings/__tests__/storage-sync-screen.test.tsx @@ -151,7 +151,7 @@ describe('StorageSyncScreen', () => { expect(screen.getByTestId('storage-pending-summary')).toHaveTextContent('storage_sync.pending_summary [unsent=2,failed=1,stuck=1]'); expect(screen.getByTestId('storage-item-evt-1-meta')).toHaveTextContent('storage_sync.age.minutes [count=5] · storage_sync.attempts [count=1]'); expect(screen.getByTestId('storage-item-evt-2-state')).toHaveTextContent('storage_sync.state.dead'); - expect(screen.getByTestId('storage-item-evt-2-error')).toHaveTextContent('storage_sync.last_error [error=validation_failed]'); + expect(screen.getByTestId('storage-item-evt-2-error')).toHaveTextContent('storage_sync.last_error [error=HTTP 422 · validation_failed]'); expect(screen.getByTestId('storage-item-evt-3-state')).toHaveTextContent('storage_sync.state.stuck'); expect(screen.getByTestId('storage-item-evt-3-meta')).toHaveTextContent(/storage_sync\.age\.hours \[count=26\]/); expect(screen.getByTestId('storage-pending-held')).toHaveTextContent('storage_sync.held [count=2]'); diff --git a/packages/features/src/settings/lib/format.ts b/packages/features/src/settings/lib/format.ts index 8c23e544..e69a4bbf 100644 --- a/packages/features/src/settings/lib/format.ts +++ b/packages/features/src/settings/lib/format.ts @@ -1,16 +1,22 @@ +import type { OutboxError } from '@resgrid/offline'; + /** Bytes as a short size ("0 B", "512 B", "1.4 KB", "23 MB", "1.2 GB"); null when unknown (the screen shows "—"). */ export const formatBytes = (bytes: number | null | undefined): string | null => { if (bytes === null || bytes === undefined || !Number.isFinite(bytes) || bytes < 0) { return null; } + const units = ['B', 'KB', 'MB', 'GB', 'TB']; let value = bytes; let unit = 0; + while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit += 1; } + const rounded = unit === 0 || value >= 10 ? Math.round(value).toString() : value.toFixed(1); + return `${rounded} ${units[unit]}`; }; @@ -23,15 +29,34 @@ export interface AgeParts { /** How long ago, as a translation key and count (an outbox item's age, the last sync). */ export const ageParts = (ageMs: number): AgeParts => { const minutes = Math.floor(Math.max(0, ageMs) / 60_000); + if (minutes < 1) { return { key: 'just_now', count: 0 }; } + if (minutes < 60) { return { key: 'minutes', count: minutes }; } + const hours = Math.floor(minutes / 60); + if (hours < 48) { return { key: 'hours', count: hours }; } + return { key: 'days', count: Math.floor(hours / 24) }; }; + +/** Axios's catch-all codes for any 4xx / 5xx answer: they hide which status the server sent. */ +const GENERIC_HTTP_CODES = new Set(['ERR_BAD_REQUEST', 'ERR_BAD_RESPONSE']); + +/** A queued change's last error for Pending changes: the HTTP status, with the server's error code when it sent one. */ +export const outboxErrorLabel = (error: OutboxError | null | undefined): string | null => { + if (!error) { + return null; + } + + const parts = [error.status ? `HTTP ${error.status}` : null, error.code && !GENERIC_HTTP_CODES.has(error.code) ? error.code : null].filter((part): part is string => !!part); + + return parts.length > 0 ? parts.join(' · ') : error.message || null; +}; diff --git a/packages/features/src/settings/screens/storage-sync-screen.tsx b/packages/features/src/settings/screens/storage-sync-screen.tsx index 3d68a8bc..4a393549 100644 --- a/packages/features/src/settings/screens/storage-sync-screen.tsx +++ b/packages/features/src/settings/screens/storage-sync-screen.tsx @@ -7,7 +7,7 @@ import React, { useCallback, useState } from 'react'; import { SettingsCard, SettingsRow } from '../components/settings-row'; import { type SyncNowOutcome, useStorageSync } from '../hooks/use-storage-sync'; -import { ageParts, formatBytes } from '../lib/format'; +import { ageParts, formatBytes, outboxErrorLabel } from '../lib/format'; type Confirmation = { kind: 'discard'; item: OutboxItemView } | { kind: 'clear-cached' } | { kind: 'clear-all'; unsent: number } | null; @@ -21,7 +21,7 @@ const canRetry = (item: OutboxItemView): boolean => item.state === 'failed' || i const PendingItem: React.FC<{ item: OutboxItemView; busy: boolean; onRetry: () => void; onConfirm: () => void; onDiscard: () => void }> = ({ item, busy, onRetry, onConfirm, onDiscard }) => { const { t } = useTranslation(); const age = ageParts(item.ageMs); - const error = item.lastError ? (item.lastError.code ?? (item.lastError.status ? `HTTP ${item.lastError.status}` : item.lastError.message)) : null; + const error = outboxErrorLabel(item.lastError); const typeLabel = t(typeKey(item.type), { defaultValue: item.type }); return ( diff --git a/packages/models/src/__tests__/models.test.ts b/packages/models/src/__tests__/models.test.ts index 18ff2d5a..e01114d9 100644 --- a/packages/models/src/__tests__/models.test.ts +++ b/packages/models/src/__tests__/models.test.ts @@ -166,6 +166,11 @@ describe('helpers', () => { expect(getIncidentRoleCapabilities(R.IncidentCommander)).toBe(C.All); expect(hasIncidentCapability(getIncidentRoleCapabilities(R.SafetyOfficer), C.ManageAnnotations)).toBe(true); expect(hasIncidentCapability(getIncidentRoleCapabilities(R.SafetyOfficer), C.ManageCommand)).toBe(false); + // Core's flags and map (Resgrid.Model.IncidentCapabilities / IncidentRoleCapabilityMap), notes and public information included. + expect(C.All).toBe(16383); + expect(hasIncidentCapability(getIncidentRoleCapabilities(R.SafetyOfficer), C.ManageNotes)).toBe(true); + expect(hasIncidentCapability(getIncidentRoleCapabilities(R.PublicInformationOfficer), C.ManagePublicInformation)).toBe(true); + expect(hasIncidentCapability(getIncidentRoleCapabilities(R.FinanceAdminSectionChief), C.ManageNotes)).toBe(false); expect(models.TacticalObjectiveStatus.InProgress).toBe(2); expect(models.IncidentNeedCategory.Entity).toBe(6); expect(models.ParStatus.Critical).toBe('Critical'); diff --git a/packages/models/src/v4/incidentCommand/incidentCommandEnums.ts b/packages/models/src/v4/incidentCommand/incidentCommandEnums.ts index f80849f9..9ce4585a 100644 --- a/packages/models/src/v4/incidentCommand/incidentCommandEnums.ts +++ b/packages/models/src/v4/incidentCommand/incidentCommandEnums.ts @@ -203,8 +203,27 @@ export enum IncidentCapabilities { ManageChannels = 256, ManageResources = 512, ViewReports = 1024, + /** Operational, situation and safety notes. */ + ManageNotes = 2048, + /** Incident-level files and records. */ + ManageDocuments = 4096, + /** Public notes, documents and the share-link lifecycle. */ + ManagePublicInformation = 8192, - All = ViewBoard | ManageCommand | ManageStructure | AssignResources | ManageObjectives | ManageTimers | ManageAnnotations | ManageAccountability | ManageChannels | ManageResources | ViewReports, + All = ViewBoard | + ManageCommand | + ManageStructure | + AssignResources | + ManageObjectives | + ManageTimers | + ManageAnnotations | + ManageAccountability | + ManageChannels | + ManageResources | + ViewReports | + ManageNotes | + ManageDocuments | + ManagePublicInformation, } /** True when the given effective capability bitmask contains the requested capability flag. */ @@ -225,18 +244,19 @@ export const getIncidentRoleCapabilities = (role: IncidentRoleType): IncidentCap return C.ViewBoard | C.ManageStructure | C.AssignResources | C.ManageObjectives | C.ManageTimers | C.ManageResources; case IncidentRoleType.PlanningSectionChief: case IncidentRoleType.SituationUnitLeader: - return C.ViewBoard | C.ManageObjectives | C.ManageAnnotations | C.ViewReports; + return C.ViewBoard | C.ManageObjectives | C.ManageAnnotations | C.ManageNotes | C.ManageDocuments | C.ViewReports; case IncidentRoleType.DocumentationUnitLeader: - return C.ViewBoard | C.ViewReports; + return C.ViewBoard | C.ManageNotes | C.ManageDocuments | C.ViewReports; case IncidentRoleType.LogisticsSectionChief: return C.ViewBoard | C.ManageChannels | C.ManageResources; case IncidentRoleType.CommunicationsUnitLeader: return C.ViewBoard | C.ManageChannels; case IncidentRoleType.FinanceAdminSectionChief: - case IncidentRoleType.PublicInformationOfficer: return C.ViewBoard | C.ViewReports; + case IncidentRoleType.PublicInformationOfficer: + return C.ViewBoard | C.ManageNotes | C.ManageDocuments | C.ManagePublicInformation | C.ViewReports; case IncidentRoleType.SafetyOfficer: - return C.ViewBoard | C.ManageAnnotations | C.ManageObjectives; + return C.ViewBoard | C.ManageAnnotations | C.ManageObjectives | C.ManageNotes; case IncidentRoleType.LiaisonOfficer: return C.ViewBoard | C.ManageResources; case IncidentRoleType.StagingAreaManager: @@ -264,7 +284,7 @@ export const getIncidentRoleCapabilities = (role: IncidentRoleType): IncidentCap return C.ViewBoard | C.AssignResources | C.ManageObjectives | C.ManageAnnotations; case IncidentRoleType.ShelterMassCareCoordinator: case IncidentRoleType.DamageAssessmentLead: - return C.ViewBoard | C.ManageObjectives | C.ViewReports; + return C.ViewBoard | C.ManageObjectives | C.ManageNotes | C.ManageDocuments | C.ViewReports; default: return C.ViewBoard; } diff --git a/packages/models/src/v4/incidentCommand/incidentCommandModels.ts b/packages/models/src/v4/incidentCommand/incidentCommandModels.ts index 80ca9d5d..0a8567ba 100644 --- a/packages/models/src/v4/incidentCommand/incidentCommandModels.ts +++ b/packages/models/src/v4/incidentCommand/incidentCommandModels.ts @@ -435,7 +435,8 @@ export type CommandTimelineResult = V4Result; export type IncidentCommandActionResult = V4Result; export type IncidentRoleResult = V4Result; export type IncidentRolesResult = V4Result; -export type IncidentCapabilitiesResult = V4Result<{ Value: number; Capabilities: string[] }>; +/** `IncidentRoles/GetMyCapabilities`: the bitmask and flag names sit on the envelope itself (no `Data`), as Core sends them. */ +export type IncidentCapabilitiesResult = BaseV4Request & { Value?: number; Capabilities?: string[] }; export type AdHocUnitResult = V4Result; export type AdHocUnitsResult = V4Result; export type AdHocPersonnelResult = V4Result; diff --git a/packages/models/src/v4/security/departmentRightsResultData.ts b/packages/models/src/v4/security/departmentRightsResultData.ts index 6229d97f..d5e11b24 100644 --- a/packages/models/src/v4/security/departmentRightsResultData.ts +++ b/packages/models/src/v4/security/departmentRightsResultData.ts @@ -37,6 +37,11 @@ export class DepartmentRightsResultData { * when the department has never configured the permission. */ public CanLoginToCommandApp: boolean = true; + /** + * Whether this user may read every command board in the department (View Command Boards). Without it they still read + * the boards of incidents they are on. Undefined from servers that predate the field, where CanLoginToCommandApp decided. + */ + public CanViewCommandBoards?: boolean; public Groups: GroupRightResultData[] = []; // Group Rights } diff --git a/tooling/oxlint/anti-slop-baseline.json b/tooling/oxlint/anti-slop-baseline.json index 419e77de..8dcc3f00 100644 --- a/tooling/oxlint/anti-slop-baseline.json +++ b/tooling/oxlint/anti-slop-baseline.json @@ -1475,7 +1475,6 @@ "anti-slop/no-chained-type-assertions": 1, "anti-slop/no-known-value-widening": 1, "anti-slop/no-unsafe-dictionary-type": 1, - "anti-slop/require-readable-spacing": 3, "anti-slop/require-safety-comment-for-type-assertion": 15 }, "packages/data/src/incident-command/__tests__/command-support-store.test.ts": { @@ -1487,7 +1486,6 @@ "anti-slop/no-chained-type-assertions": 18, "anti-slop/no-unknown-returns": 1, "anti-slop/no-unsafe-dictionary-type": 1, - "anti-slop/require-readable-spacing": 8, "anti-slop/require-safety-comment-for-type-assertion": 37 }, "packages/data/src/incident-command/command-store.ts": { @@ -2691,9 +2689,6 @@ "anti-slop/no-known-value-widening": 1, "anti-slop/require-readable-spacing": 7 }, - "packages/features/src/incident-command/hooks/use-command-permissions.ts": { - "anti-slop/require-readable-spacing": 1 - }, "packages/features/src/incident-command/hooks/use-incident-chat.ts": { "anti-slop/require-readable-spacing": 4 }, @@ -2729,7 +2724,7 @@ }, "packages/features/src/incident-command/lib/permissions.ts": { "anti-slop/no-runtime-typeof": 1, - "anti-slop/require-readable-spacing": 4 + "anti-slop/require-readable-spacing": 1 }, "packages/features/src/incident-command/routes.ts": { "anti-slop/require-readable-spacing": 3 @@ -3261,9 +3256,6 @@ "packages/features/src/settings/index.ts": { "anti-slop/require-readable-spacing": 10 }, - "packages/features/src/settings/lib/format.ts": { - "anti-slop/require-readable-spacing": 9 - }, "packages/features/src/settings/lib/navigation-app.ts": { "anti-slop/require-readable-spacing": 5 }, diff --git a/tsconfig.tsbuildinfo b/tsconfig.tsbuildinfo new file mode 100644 index 00000000..bdf58348 --- /dev/null +++ b/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./packages/api/src/index.ts","./packages/api/src/registry.ts","./packages/api/src/__tests__/contract.test.ts","./packages/api/src/__tests__/helpers.ts","./packages/api/src/core/api-error.ts","./packages/api/src/core/binary.ts","./packages/api/src/core/cache.ts","./packages/api/src/core/dates.ts","./packages/api/src/core/endpoint.ts","./packages/api/src/core/http-client.ts","./packages/api/src/core/index.ts","./packages/api/src/core/request.ts","./packages/api/src/core/shared-session.ts","./packages/api/src/core/url.ts","./packages/api/src/core/v4-result.ts","./packages/api/src/core/__tests__/cache.test.ts","./packages/api/src/core/__tests__/http-client.test.ts","./packages/api/src/core/__tests__/request.test.ts","./packages/api/src/core/__tests__/utils.test.ts","./packages/api/src/endpoints/index.ts","./packages/api/src/endpoints/__tests__/endpoints.test.ts","./packages/api/src/endpoints/auth-discovery/auth-discovery.ts","./packages/api/src/endpoints/auth-discovery/index.ts","./packages/api/src/endpoints/awaiting-resources/awaiting-resources.ts","./packages/api/src/endpoints/awaiting-resources/index.ts","./packages/api/src/endpoints/calendar/calendar.ts","./packages/api/src/endpoints/calendar/index.ts","./packages/api/src/endpoints/call-files/call-file-helpers.ts","./packages/api/src/endpoints/call-files/call-files.ts","./packages/api/src/endpoints/call-files/index.ts","./packages/api/src/endpoints/call-video-feeds/call-video-feeds.ts","./packages/api/src/endpoints/call-video-feeds/index.ts","./packages/api/src/endpoints/calls/call-requests.ts","./packages/api/src/endpoints/calls/calls.ts","./packages/api/src/endpoints/calls/index.ts","./packages/api/src/endpoints/certifications/certifications.ts","./packages/api/src/endpoints/certifications/index.ts","./packages/api/src/endpoints/chat/chat-attachments.ts","./packages/api/src/endpoints/chat/chat.ts","./packages/api/src/endpoints/chat/index.ts","./packages/api/src/endpoints/check-in-timers/check-in-timers.ts","./packages/api/src/endpoints/check-in-timers/index.ts","./packages/api/src/endpoints/checklists/checklist-images.ts","./packages/api/src/endpoints/checklists/checklists.ts","./packages/api/src/endpoints/checklists/index.ts","./packages/api/src/endpoints/communication-tests/communication-tests.ts","./packages/api/src/endpoints/communication-tests/index.ts","./packages/api/src/endpoints/config/config.ts","./packages/api/src/endpoints/config/index.ts","./packages/api/src/endpoints/contacts/contacts.ts","./packages/api/src/endpoints/contacts/index.ts","./packages/api/src/endpoints/counts/counts.ts","./packages/api/src/endpoints/counts/index.ts","./packages/api/src/endpoints/data-protection/data-protection.ts","./packages/api/src/endpoints/data-protection/index.ts","./packages/api/src/endpoints/devices/devices.ts","./packages/api/src/endpoints/devices/index.ts","./packages/api/src/endpoints/devices/push-registration.ts","./packages/api/src/endpoints/dispatch/dispatch.ts","./packages/api/src/endpoints/dispatch/index.ts","./packages/api/src/endpoints/feature-flags/feature-flags.ts","./packages/api/src/endpoints/feature-flags/index.ts","./packages/api/src/endpoints/forms/forms.ts","./packages/api/src/endpoints/forms/index.ts","./packages/api/src/endpoints/geocoding/geocoding.ts","./packages/api/src/endpoints/geocoding/index.ts","./packages/api/src/endpoints/groups/groups.ts","./packages/api/src/endpoints/groups/index.ts","./packages/api/src/endpoints/health/health.ts","./packages/api/src/endpoints/health/index.ts","./packages/api/src/endpoints/inbox/inbox.ts","./packages/api/src/endpoints/inbox/index.ts","./packages/api/src/endpoints/incident-command/incident-command.ts","./packages/api/src/endpoints/incident-command/index.ts","./packages/api/src/endpoints/inventory/index.ts","./packages/api/src/endpoints/inventory/inventory.ts","./packages/api/src/endpoints/mapping/index.ts","./packages/api/src/endpoints/mapping/mapping.ts","./packages/api/src/endpoints/messages/index.ts","./packages/api/src/endpoints/messages/messages.ts","./packages/api/src/endpoints/notes/index.ts","./packages/api/src/endpoints/notes/notes.ts","./packages/api/src/endpoints/operations/index.ts","./packages/api/src/endpoints/operations/operations.ts","./packages/api/src/endpoints/personnel/index.ts","./packages/api/src/endpoints/personnel/personnel.ts","./packages/api/src/endpoints/protocols/index.ts","./packages/api/src/endpoints/protocols/protocols.ts","./packages/api/src/endpoints/records/index.ts","./packages/api/src/endpoints/records/records.ts","./packages/api/src/endpoints/routes/index.ts","./packages/api/src/endpoints/routes/routes.ts","./packages/api/src/endpoints/routing/index.ts","./packages/api/src/endpoints/routing/routing.ts","./packages/api/src/endpoints/run-cards/index.ts","./packages/api/src/endpoints/run-cards/run-cards.ts","./packages/api/src/endpoints/security/index.ts","./packages/api/src/endpoints/security/security.ts","./packages/api/src/endpoints/shifts/index.ts","./packages/api/src/endpoints/shifts/shifts.ts","./packages/api/src/endpoints/statuses/index.ts","./packages/api/src/endpoints/statuses/statuses.ts","./packages/api/src/endpoints/sync/index.ts","./packages/api/src/endpoints/sync/sync.ts","./packages/api/src/endpoints/units/index.ts","./packages/api/src/endpoints/units/units.ts","./packages/api/src/endpoints/user-defined-fields/index.ts","./packages/api/src/endpoints/user-defined-fields/user-defined-fields.ts","./packages/api/src/endpoints/voice/index.ts","./packages/api/src/endpoints/voice/voice.ts","./packages/api/src/endpoints/weather-alerts/index.ts","./packages/api/src/endpoints/weather-alerts/weather-alert-errors.ts","./packages/api/src/endpoints/weather-alerts/weather-alerts.ts","./packages/api/src/endpoints/work-orders/index.ts","./packages/api/src/endpoints/work-orders/work-order-images.ts","./packages/api/src/endpoints/work-orders/work-orders.ts","./packages/app-shell/src/configure.ts","./packages/app-shell/src/home-placeholder.tsx","./packages/app-shell/src/home-route.tsx","./packages/app-shell/src/i18n.ts","./packages/app-shell/src/index.ts","./packages/app-shell/src/root-layout.tsx","./packages/app-shell/src/__tests__/configure.test.ts","./packages/app-shell/src/__tests__/home-route.test.tsx","./packages/app-shell/src/__tests__/nav-badges.test.ts","./packages/app-shell/src/__tests__/qa-flag-gated-nav.test.ts","./packages/app-shell/src/__tests__/root-layout.test.tsx","./packages/app-shell/src/__tests__/smoke.test.ts","./packages/app-shell/src/__tests__/helpers/variants.ts","./packages/app-shell/src/__tests__/mocks/expo-router.tsx","./packages/app-shell/src/__tests__/mocks/platform.ts","./packages/app-shell/src/deep-links/navigation.ts","./packages/app-shell/src/gate/fallback-screens.tsx","./packages/app-shell/src/gate/gate-state.ts","./packages/app-shell/src/gate/shell-gate.tsx","./packages/app-shell/src/gate/use-gate-step.ts","./packages/app-shell/src/gate/__tests__/gate-state.test.ts","./packages/app-shell/src/gate/__tests__/shell-gate.test.tsx","./packages/app-shell/src/init/app-init.ts","./packages/app-shell/src/init/create-app-init.ts","./packages/app-shell/src/init/default-steps.ts","./packages/app-shell/src/init/voice-session.ts","./packages/app-shell/src/init/__tests__/create-app-init.test.ts","./packages/app-shell/src/init/__tests__/session-steps.test.ts","./packages/app-shell/src/init/__tests__/startup-steps.test.ts","./packages/app-shell/src/init/__tests__/voice-session.test.ts","./packages/app-shell/src/nav/define-nav.ts","./packages/app-shell/src/nav/feature-nav.ts","./packages/app-shell/src/nav/filter.ts","./packages/app-shell/src/nav/index.ts","./packages/app-shell/src/nav/use-nav-items.ts","./packages/app-shell/src/nav/__tests__/nav.test.ts","./packages/app-shell/src/providers/inbox-provider.tsx","./packages/app-shell/src/providers/push-modal-host.tsx","./packages/app-shell/src/providers/root-providers.tsx","./packages/app-shell/src/providers/__tests__/inbox-provider.test.tsx","./packages/app-shell/src/providers/__tests__/root-providers.test.tsx","./packages/app-shell/src/routes/canonical.ts","./packages/app-shell/src/routes/define-routes.ts","./packages/app-shell/src/routes/href.ts","./packages/app-shell/src/routes/index.ts","./packages/app-shell/src/routes/notification-routes.ts","./packages/app-shell/src/routes/__tests__/routes.test.tsx","./packages/app-shell/src/runtime/active-session.ts","./packages/app-shell/src/runtime/api-cache-bridge.ts","./packages/app-shell/src/runtime/preferences.ts","./packages/app-shell/src/runtime/registry-store.ts","./packages/app-shell/src/runtime/runtime-holder.ts","./packages/app-shell/src/runtime/session.ts","./packages/app-shell/src/runtime/shell-config.ts","./packages/app-shell/src/runtime/shell-state.ts","./packages/app-shell/src/runtime/slots.ts","./packages/app-shell/src/runtime/__tests__/active-session.test.ts","./packages/app-shell/src/runtime/__tests__/session.test.ts","./packages/app-shell/src/runtime/__tests__/shell-providers.test.tsx","./packages/app-shell/src/session/session-services.tsx","./packages/app-shell/src/shell/badge-counts.tsx","./packages/app-shell/src/shell/create-app-shell.tsx","./packages/app-shell/src/shell/header.tsx","./packages/app-shell/src/shell/overlays.tsx","./packages/app-shell/src/shell/registered-providers.tsx","./packages/app-shell/src/shell/shell-context.tsx","./packages/app-shell/src/shell/shell-stack.tsx","./packages/app-shell/src/shell/sidebar-mode.ts","./packages/app-shell/src/shell/sidebar.tsx","./packages/app-shell/src/shell/__tests__/create-app-shell.test.tsx","./packages/app-shell/src/telemetry/screen-views.ts","./packages/app-shell/src/telemetry/__tests__/screen-views.test.tsx","./packages/app-shell/src/testing/route-contract.ts","./packages/app-shell/src/testing/variant-contract.ts","./packages/app-shell/src/testing/__tests__/route-contract.test.ts","./packages/app-shell/src/testing/__tests__/variant-contract.test.ts","./packages/app-shell/src/testing/__tests__/variant-routes.test.ts","./packages/app-shell/src/updates/update-notice.tsx","./packages/app-shell/src/updates/update-steps.ts","./packages/app-shell/src/updates/use-wall-auto-restart.ts","./packages/app-shell/src/updates/wall-auto-restart.ts","./packages/app-shell/src/updates/__tests__/update-notice.test.tsx","./packages/app-shell/src/updates/__tests__/update-steps.test.ts","./packages/auth/src/hooks.ts","./packages/auth/src/index.ts","./packages/auth/src/init-auth.ts","./packages/auth/src/session-gates.ts","./packages/auth/src/__tests__/auth-session.test.ts","./packages/auth/src/__tests__/contract.test.ts","./packages/auth/src/__tests__/data-protection.test.ts","./packages/auth/src/__tests__/dev-proxy.test.ts","./packages/auth/src/__tests__/errors.test.ts","./packages/auth/src/__tests__/hooks.test.tsx","./packages/auth/src/__tests__/init-and-approver.test.ts","./packages/auth/src/__tests__/login-mfa.test.ts","./packages/auth/src/__tests__/passkey-errors.test.ts","./packages/auth/src/__tests__/personal-lock.test.ts","./packages/auth/src/__tests__/rights-flags-terminology.test.ts","./packages/auth/src/__tests__/server-url.test.ts","./packages/auth/src/__tests__/shared-session.test.ts","./packages/auth/src/__tests__/sso.test.ts","./packages/auth/src/__tests__/token-refresh.test.ts","./packages/auth/src/__tests__/fakes/expo-application.ts","./packages/auth/src/__tests__/fakes/expo-auth-session.ts","./packages/auth/src/__tests__/fakes/expo-crypto.ts","./packages/auth/src/__tests__/fakes/expo-device.ts","./packages/auth/src/__tests__/fakes/expo-linking.ts","./packages/auth/src/__tests__/fakes/expo-web-browser.ts","./packages/auth/src/__tests__/fakes/react-native-passkey.ts","./packages/auth/src/__tests__/fakes/react-native.ts","./packages/auth/src/__tests__/helpers/fake-server.ts","./packages/auth/src/__tests__/helpers/harness.ts","./packages/auth/src/__tests__/helpers/jwt.ts","./packages/auth/src/__tests__/helpers/variant.ts","./packages/auth/src/api/account-security.ts","./packages/auth/src/api/auth-request.ts","./packages/auth/src/api/connect.ts","./packages/auth/src/api/endpoints.ts","./packages/auth/src/api/shared-session.ts","./packages/auth/src/api/sso-discovery.ts","./packages/auth/src/api/transaction.ts","./packages/auth/src/context/authenticated-api-context.ts","./packages/auth/src/data-protection/grant-provider.ts","./packages/auth/src/data-protection/grant-store.ts","./packages/auth/src/data-protection/redacted.ts","./packages/auth/src/data-protection/use-protected-grant-refresh.ts","./packages/auth/src/errors/http-shape.ts","./packages/auth/src/errors/login-errors.ts","./packages/auth/src/errors/mfa-errors.ts","./packages/auth/src/errors/mfa-messages.ts","./packages/auth/src/flags/feature-flags-store.ts","./packages/auth/src/mfa/approval-wait.ts","./packages/auth/src/mfa/approver.ts","./packages/auth/src/mfa/base64url.ts","./packages/auth/src/mfa/login-mfa.ts","./packages/auth/src/mfa/pkce.ts","./packages/auth/src/mfa/types.ts","./packages/auth/src/passkeys/passkey-errors.ts","./packages/auth/src/passkeys/passkey-policy.ts","./packages/auth/src/passkeys/passkey.ts","./packages/auth/src/passkeys/passkey.web.ts","./packages/auth/src/personal-lock/inactivity-monitor.ts","./packages/auth/src/personal-lock/password-verifier.ts","./packages/auth/src/personal-lock/personal-lock-store.ts","./packages/auth/src/rights/app-access.ts","./packages/auth/src/rights/security-store.ts","./packages/auth/src/runtime/api-context.ts","./packages/auth/src/runtime/crypto.ts","./packages/auth/src/runtime/device-info.ts","./packages/auth/src/runtime/logger.ts","./packages/auth/src/runtime/platform.ts","./packages/auth/src/runtime/services.ts","./packages/auth/src/runtime/storage.ts","./packages/auth/src/runtime/telemetry.ts","./packages/auth/src/runtime/variant.ts","./packages/auth/src/session/auth-store.ts","./packages/auth/src/session/client-headers.ts","./packages/auth/src/session/hydrate-session.ts","./packages/auth/src/session/jwt.ts","./packages/auth/src/session/server-url.ts","./packages/auth/src/session/shared-installation.ts","./packages/auth/src/session/sign-out-hooks.ts","./packages/auth/src/session/switch-server.ts","./packages/auth/src/session/token-refresh.ts","./packages/auth/src/session/types.ts","./packages/auth/src/shared-session/shared-session-controller.ts","./packages/auth/src/shared-session/shared-session-store.ts","./packages/auth/src/sso/legacy-sso-desktop.ts","./packages/auth/src/sso/oidc.ts","./packages/auth/src/sso/saml.ts","./packages/auth/src/sso/sso-browser.ts","./packages/auth/src/sso/sso-url.ts","./packages/auth/src/terminology/terminology-store.ts","./packages/config/src/hooks.ts","./packages/config/src/index.ts","./packages/config/src/runtime.ts","./packages/config/src/schema.ts","./packages/config/src/versioning.ts","./packages/config/src/__tests__/manifests.test.ts","./packages/config/src/__tests__/runtime.test.ts","./packages/core/src/dev.ts","./packages/core/src/index.ts","./packages/core/src/analytics/analytics-service.ts","./packages/core/src/analytics/countly-client.ts","./packages/core/src/analytics/countly-client.web.ts","./packages/core/src/analytics/types.ts","./packages/core/src/analytics/__tests__/analytics-service.test.ts","./packages/core/src/analytics/__tests__/countly-client.test.ts","./packages/core/src/analytics/__tests__/countly-client.web.test.ts","./packages/core/src/device/native-device.ts","./packages/core/src/device/__tests__/native-device.test.ts","./packages/core/src/env/dev-proxy.ts","./packages/core/src/env/env.ts","./packages/core/src/env/__tests__/dev-proxy.test.ts","./packages/core/src/env/__tests__/env.test.ts","./packages/core/src/lifecycle/app-lifecycle.ts","./packages/core/src/lifecycle/use-now.ts","./packages/core/src/lifecycle/user-activity.ts","./packages/core/src/lifecycle/__tests__/app-lifecycle.test.ts","./packages/core/src/lifecycle/__tests__/user-activity.test.ts","./packages/core/src/logging/logger.ts","./packages/core/src/logging/sanitize.ts","./packages/core/src/logging/types.ts","./packages/core/src/logging/__tests__/logger.test.ts","./packages/core/src/logging/__tests__/sanitize.test.ts","./packages/core/src/navigation/route-paths.ts","./packages/core/src/navigation/router.ts","./packages/core/src/navigation/__tests__/navigation-ready.test.ts","./packages/core/src/navigation/__tests__/route-paths.test.ts","./packages/core/src/platform/platform.ts","./packages/core/src/platform/__tests__/platform.test.ts","./packages/core/src/sentry/filters.ts","./packages/core/src/sentry/sentry.ts","./packages/core/src/sentry/__tests__/filters.test.ts","./packages/core/src/sentry/__tests__/sentry.test.ts","./packages/core/src/updates/app-updates.ts","./packages/core/src/updates/types.ts","./packages/core/src/updates/use-app-updates.ts","./packages/core/src/updates/__tests__/app-updates.test.ts","./packages/core/src/utils/dates.ts","./packages/core/src/utils/keep-alive.ts","./packages/core/src/utils/request-errors.ts","./packages/core/src/utils/route-ids.ts","./packages/core/src/utils/single-flight.ts","./packages/core/src/utils/__tests__/dates.test.ts","./packages/core/src/utils/__tests__/keep-alive.test.ts","./packages/core/src/utils/__tests__/request-errors.test.ts","./packages/core/src/utils/__tests__/route-ids.test.ts","./packages/core/src/utils/__tests__/single-flight.test.ts","./packages/data/src/configure.ts","./packages/data/src/index.ts","./packages/data/src/__tests__/reference-stores.test.ts","./packages/data/src/__tests__/helpers/index.ts","./packages/data/src/awaiting-resources/awaiting-resources-alerts.ts","./packages/data/src/awaiting-resources/awaiting-resources-store.ts","./packages/data/src/awaiting-resources/index.ts","./packages/data/src/awaiting-resources/__tests__/awaiting-resources-store.test.ts","./packages/data/src/badge-counts/badge-counts-store.ts","./packages/data/src/badge-counts/__tests__/badge-counts-store.test.ts","./packages/data/src/cal-oes-mars/cal-oes-mars-store.ts","./packages/data/src/cal-oes-mars/__tests__/cal-oes-mars-store.test.ts","./packages/data/src/calendar/calendar-store.ts","./packages/data/src/call-queues/archived-calls-store.ts","./packages/data/src/call-queues/call-queue-actions-store.ts","./packages/data/src/call-queues/index.ts","./packages/data/src/call-queues/__tests__/call-queues.test.ts","./packages/data/src/calls/call-create-handler.ts","./packages/data/src/calls/call-detail-store.ts","./packages/data/src/calls/call-order.ts","./packages/data/src/calls/call-video-feeds-store.ts","./packages/data/src/calls/call-write-handlers.ts","./packages/data/src/calls/calls-store.ts","./packages/data/src/calls/check-in-timers-store.ts","./packages/data/src/calls/__tests__/call-detail-and-timers.test.ts","./packages/data/src/calls/__tests__/call-order.test.ts","./packages/data/src/calls/__tests__/calls-store.test.ts","./packages/data/src/certifications/certifications-store.ts","./packages/data/src/chat/chat-store.ts","./packages/data/src/chat/__tests__/chat-store.test.ts","./packages/data/src/chat/__tests__/chatbot-store.test.ts","./packages/data/src/checklists/checklists-store.ts","./packages/data/src/contacts/contacts-store.ts","./packages/data/src/core/envelope.ts","./packages/data/src/core/errors.ts","./packages/data/src/core/keyed-flight.ts","./packages/data/src/core/outbox.ts","./packages/data/src/core/persist.ts","./packages/data/src/core/reset.ts","./packages/data/src/core/runtime.ts","./packages/data/src/core/slices.ts","./packages/data/src/core/status-fields.ts","./packages/data/src/core/__tests__/core.test.ts","./packages/data/src/incident-command/command-handlers.ts","./packages/data/src/incident-command/command-store.ts","./packages/data/src/incident-command/command-support-store.ts","./packages/data/src/incident-command/resource-view-store.ts","./packages/data/src/incident-command/__tests__/command-store.test.ts","./packages/data/src/incident-command/__tests__/command-support-store.test.ts","./packages/data/src/inventory/inventory-store.ts","./packages/data/src/inventory/__tests__/inventory-store.test.ts","./packages/data/src/map-markers/index.ts","./packages/data/src/map-markers/map-markers-store.ts","./packages/data/src/map-markers/__tests__/map-markers-store.test.ts","./packages/data/src/mapping/custom-maps-store.ts","./packages/data/src/messages/messages-store.ts","./packages/data/src/navigation/index.ts","./packages/data/src/navigation/navigation-eta-store.ts","./packages/data/src/navigation/navigation-eta.ts","./packages/data/src/navigation/__tests__/navigation-eta-store.test.ts","./packages/data/src/notes/notes-store.ts","./packages/data/src/offline-scope/register.ts","./packages/data/src/offline-scope/scope.ts","./packages/data/src/offline-scope/__tests__/scope.test.ts","./packages/data/src/operations/operations-store.ts","./packages/data/src/personnel/personnel-status-store.ts","./packages/data/src/personnel/personnel-store.ts","./packages/data/src/personnel/__tests__/personnel.test.ts","./packages/data/src/pois/pois-store.ts","./packages/data/src/protocols/protocol-wire.ts","./packages/data/src/protocols/protocols-store.ts","./packages/data/src/protocols/__tests__/protocols-store.test.ts","./packages/data/src/records/record-deployments-store.ts","./packages/data/src/records/records-store.ts","./packages/data/src/records/__tests__/records-store.test.ts","./packages/data/src/routes/routes-store.ts","./packages/data/src/routes/__tests__/routes-store-history.test.ts","./packages/data/src/routes/__tests__/routes-store.test.ts","./packages/data/src/run-cards/index.ts","./packages/data/src/run-cards/run-cards-store.ts","./packages/data/src/run-cards/__tests__/run-cards-store.test.ts","./packages/data/src/shifts/shifts-store.ts","./packages/data/src/shifts/__tests__/shifts-store.test.ts","./packages/data/src/unit-status-alerts/unit-status-alerts-store.ts","./packages/data/src/unit-status-alerts/__tests__/unit-status-alerts.test.ts","./packages/data/src/units/unit-roles-store.ts","./packages/data/src/units/unit-status-store.ts","./packages/data/src/units/units-store.ts","./packages/data/src/units/__tests__/units.test.ts","./packages/data/src/weather-alerts/weather-alerts-store.ts","./packages/data/src/weather-alerts/__tests__/weather-alerts.test.ts","./packages/data/src/work-orders/work-orders-store.ts","./packages/data/src/work-orders/__tests__/work-orders-store.test.ts","./packages/features/src/index.ts","./packages/features/src/__tests__/list-card-surface.test.ts","./packages/features/src/__tests__/new-item-fab.test.ts","./packages/features/src/__tests__/route-builders.test.ts","./packages/features/src/__tests__/zero-state-retry.test.ts","./packages/features/src/approve-sign-in/index.ts","./packages/features/src/approve-sign-in/nav.ts","./packages/features/src/approve-sign-in/routes.ts","./packages/features/src/approve-sign-in/__tests__/approve-sign-in.test.tsx","./packages/features/src/approve-sign-in/components/pending-approval-watcher.tsx","./packages/features/src/approve-sign-in/hooks/use-pending-approval-check.ts","./packages/features/src/auth-ui/index.ts","./packages/features/src/auth-ui/nav.ts","./packages/features/src/auth-ui/routes.ts","./packages/features/src/auth-ui/__tests__/account-security.test.tsx","./packages/features/src/auth-ui/__tests__/approve-sign-in.test.tsx","./packages/features/src/auth-ui/__tests__/auth-fake.ts","./packages/features/src/auth-ui/__tests__/gate-screens.test.tsx","./packages/features/src/auth-ui/__tests__/login-mfa-sheet.test.tsx","./packages/features/src/auth-ui/__tests__/login-screen.test.tsx","./packages/features/src/auth-ui/__tests__/mfa-messages.test.ts","./packages/features/src/auth-ui/__tests__/server-url-sheet.test.tsx","./packages/features/src/auth-ui/__tests__/shared-device-lock.test.tsx","./packages/features/src/auth-ui/__tests__/sso-sign-in.test.tsx","./packages/features/src/auth-ui/components/auth-layout.tsx","./packages/features/src/auth-ui/components/auth-layout.web.tsx","./packages/features/src/auth-ui/components/auth-logo.tsx","./packages/features/src/auth-ui/components/confirm-dialog.tsx","./packages/features/src/auth-ui/components/inline-error.tsx","./packages/features/src/auth-ui/components/language-picker.tsx","./packages/features/src/auth-ui/components/login-form.tsx","./packages/features/src/auth-ui/components/login-server-badge.tsx","./packages/features/src/auth-ui/components/option-sheet.tsx","./packages/features/src/auth-ui/components/password-input.tsx","./packages/features/src/auth-ui/components/server-url-sheet.tsx","./packages/features/src/auth-ui/components/shared-device-setup.tsx","./packages/features/src/auth-ui/components/sso-sign-in.tsx","./packages/features/src/auth-ui/lib/api-context.ts","./packages/features/src/auth-ui/lib/auth-errors.ts","./packages/features/src/auth-ui/lib/device-kv.ts","./packages/features/src/auth-ui/lib/languages.ts","./packages/features/src/auth-ui/lib/login-server.ts","./packages/features/src/auth-ui/lib/navigation.ts","./packages/features/src/auth-ui/mfa/account-verify-sheet.tsx","./packages/features/src/auth-ui/mfa/approval-panel.tsx","./packages/features/src/auth-ui/mfa/legacy-otp-sheet.tsx","./packages/features/src/auth-ui/mfa/login-mfa-sheet.tsx","./packages/features/src/auth-ui/mfa/recovery-codes.tsx","./packages/features/src/auth-ui/screens/access-denied-screen.tsx","./packages/features/src/auth-ui/screens/account-security-screen.tsx","./packages/features/src/auth-ui/screens/approve-sign-in-screen.tsx","./packages/features/src/auth-ui/screens/login-screen.tsx","./packages/features/src/auth-ui/screens/maintenance-screen.tsx","./packages/features/src/auth-ui/screens/onboarding-screen.tsx","./packages/features/src/auth-ui/screens/personal-lock-screen.tsx","./packages/features/src/auth-ui/screens/recovery-screen.tsx","./packages/features/src/auth-ui/screens/route-screens.tsx","./packages/features/src/auth-ui/screens/shared-device-lock-screen.tsx","./packages/features/src/awaiting-resources/index.ts","./packages/features/src/awaiting-resources/nav.ts","./packages/features/src/awaiting-resources/register.tsx","./packages/features/src/awaiting-resources/routes.ts","./packages/features/src/awaiting-resources/__tests__/awaiting-resources.test.tsx","./packages/features/src/awaiting-resources/components/awaiting-call-card.tsx","./packages/features/src/awaiting-resources/components/awaiting-resources-banner.tsx","./packages/features/src/awaiting-resources/components/awaiting-resources-host.tsx","./packages/features/src/awaiting-resources/components/call-outstanding-requirements.tsx","./packages/features/src/awaiting-resources/components/dismiss-requirement-dialog.tsx","./packages/features/src/awaiting-resources/components/unit-available-alert-host.tsx","./packages/features/src/awaiting-resources/hooks/use-awaiting-resources.ts","./packages/features/src/awaiting-resources/lib/awaiting-format.ts","./packages/features/src/awaiting-resources/screens/awaiting-resources.tsx","./packages/features/src/calendar/index.ts","./packages/features/src/calendar/nav.ts","./packages/features/src/calendar/routes.ts","./packages/features/src/calendar/__tests__/calendar.test.tsx","./packages/features/src/calendar/__tests__/test-setup.ts","./packages/features/src/calendar/components/calendar-card.tsx","./packages/features/src/calendar/components/calendar-item-details-sheet.tsx","./packages/features/src/calendar/components/month-calendar.tsx","./packages/features/src/calendar/lib/calendar-dates.ts","./packages/features/src/calendar/screens/calendar.tsx","./packages/features/src/call-queues/index.ts","./packages/features/src/call-queues/nav.ts","./packages/features/src/call-queues/routes.ts","./packages/features/src/call-queues/__tests__/call-queues.test.tsx","./packages/features/src/call-queues/components/queue-confirm-dialog.tsx","./packages/features/src/call-queues/components/queue-controls.tsx","./packages/features/src/call-queues/components/queue-table.tsx","./packages/features/src/call-queues/hooks/use-call-dispatch-now.tsx","./packages/features/src/call-queues/lib/archived-calls.ts","./packages/features/src/call-queues/lib/dispatch-selection.ts","./packages/features/src/call-queues/lib/queue-filters.ts","./packages/features/src/call-queues/screens/archived-calls.tsx","./packages/features/src/call-queues/screens/pending-calls.tsx","./packages/features/src/call-queues/screens/scheduled-calls.tsx","./packages/features/src/calls/hook-points.ts","./packages/features/src/calls/index.ts","./packages/features/src/calls/nav.ts","./packages/features/src/calls/routes.ts","./packages/features/src/calls/__tests__/call-card.test.tsx","./packages/features/src/calls/__tests__/call-detail.test.tsx","./packages/features/src/calls/__tests__/call-display.test.tsx","./packages/features/src/calls/__tests__/call-edit-protected.test.tsx","./packages/features/src/calls/__tests__/call-file-url.test.ts","./packages/features/src/calls/__tests__/call-form.test.tsx","./packages/features/src/calls/__tests__/call-images-modal.test.tsx","./packages/features/src/calls/__tests__/call-lib.test.ts","./packages/features/src/calls/__tests__/call-list.test.tsx","./packages/features/src/calls/__tests__/call-permissions.test.ts","./packages/features/src/calls/__tests__/call-sheets.test.tsx","./packages/features/src/calls/__tests__/call-shortcuts.test.tsx","./packages/features/src/calls/__tests__/call-summary-modal.test.tsx","./packages/features/src/calls/__tests__/department-time-zone.test.tsx","./packages/features/src/calls/__tests__/dispatch-keys.test.tsx","./packages/features/src/calls/__tests__/recipient-picker.test.ts","./packages/features/src/calls/__tests__/source-rules.test.ts","./packages/features/src/calls/__tests__/test-setup.tsx","./packages/features/src/calls/__tests__/test-utils.tsx","./packages/features/src/calls/components/add-resources-sheet.tsx","./packages/features/src/calls/components/call-audio-modal.tsx","./packages/features/src/calls/components/call-card.tsx","./packages/features/src/calls/components/call-confirm-dialog.tsx","./packages/features/src/calls/components/call-filters-bar.tsx","./packages/features/src/calls/components/call-media-modals.tsx","./packages/features/src/calls/components/call-menu.tsx","./packages/features/src/calls/components/call-modal-frame.tsx","./packages/features/src/calls/components/call-notes-panel.tsx","./packages/features/src/calls/components/call-summary-modal.tsx","./packages/features/src/calls/components/close-call-sheet.tsx","./packages/features/src/calls/components/dispatch-selection.tsx","./packages/features/src/calls/components/dispatch-ticker.tsx","./packages/features/src/calls/components/offline-call-banner.tsx","./packages/features/src/calls/components/option-chips.tsx","./packages/features/src/calls/components/queued-call-badge.tsx","./packages/features/src/calls/components/recipient-picker.tsx","./packages/features/src/calls/components/reschedule-call-sheet.tsx","./packages/features/src/calls/components/synced-call-notices.tsx","./packages/features/src/calls/components/call-form/contact-picker.tsx","./packages/features/src/calls/components/call-form/destination-poi-picker.tsx","./packages/features/src/calls/components/call-form/form-pickers.tsx","./packages/features/src/calls/components/call-form/udf-fields.tsx","./packages/features/src/calls/components/detail/call-activity-panel.tsx","./packages/features/src/calls/components/detail/call-dispatched-panel.tsx","./packages/features/src/calls/components/detail/call-info-panel.tsx","./packages/features/src/calls/components/detail/call-map-section.tsx","./packages/features/src/calls/components/detail/call-media-panel.tsx","./packages/features/src/calls/components/detail/call-protocols-panel.tsx","./packages/features/src/calls/components/detail/call-queue-banner.tsx","./packages/features/src/calls/components/detail/detail-row.tsx","./packages/features/src/calls/components/detail/location-history-panel.tsx","./packages/features/src/calls/components/detail/protected-reveal-bar.tsx","./packages/features/src/calls/components/detail/site-info-panel.tsx","./packages/features/src/calls/detail-tabs/built-in-tabs.tsx","./packages/features/src/calls/detail-tabs/registry.ts","./packages/features/src/calls/hooks/use-active-incident-command.ts","./packages/features/src/calls/hooks/use-call-assignment-context.ts","./packages/features/src/calls/hooks/use-call-dispatch-now.tsx","./packages/features/src/calls/hooks/use-call-form-data.ts","./packages/features/src/calls/hooks/use-call-list.ts","./packages/features/src/calls/hooks/use-call-permissions.ts","./packages/features/src/calls/hooks/use-call-shortcuts.ts","./packages/features/src/calls/hooks/use-call-shortcuts.web.ts","./packages/features/src/calls/hooks/use-call-udf-fields.ts","./packages/features/src/calls/hooks/use-connectivity.ts","./packages/features/src/calls/lib/call-colors.ts","./packages/features/src/calls/lib/call-dispatch.ts","./packages/features/src/calls/lib/call-display.ts","./packages/features/src/calls/lib/call-file-url.ts","./packages/features/src/calls/lib/call-filters.ts","./packages/features/src/calls/lib/call-form.ts","./packages/features/src/calls/lib/call-geocoding.ts","./packages/features/src/calls/lib/call-location.ts","./packages/features/src/calls/lib/call-permissions.ts","./packages/features/src/calls/lib/call-priority.ts","./packages/features/src/calls/lib/call-schedule.ts","./packages/features/src/calls/lib/call-time.ts","./packages/features/src/calls/lib/close-call.ts","./packages/features/src/calls/lib/dispatch-selection.ts","./packages/features/src/calls/lib/field-policy.ts","./packages/features/src/calls/lib/recipient-picker.ts","./packages/features/src/calls/lib/server-refusal.ts","./packages/features/src/calls/screens/call-detail.tsx","./packages/features/src/calls/screens/call-edit.tsx","./packages/features/src/calls/screens/call-form.tsx","./packages/features/src/calls/screens/call-list.tsx","./packages/features/src/calls/screens/call-new.tsx","./packages/features/src/certifications/index.ts","./packages/features/src/certifications/nav.ts","./packages/features/src/certifications/routes.ts","./packages/features/src/certifications/__tests__/certifications.test.tsx","./packages/features/src/certifications/components/certification-card.tsx","./packages/features/src/certifications/components/form-alert.tsx","./packages/features/src/certifications/components/form-field.tsx","./packages/features/src/certifications/components/section-card.tsx","./packages/features/src/certifications/components/status-badge.tsx","./packages/features/src/certifications/hooks/use-expiry-text.ts","./packages/features/src/certifications/lib/capture-photo.ts","./packages/features/src/certifications/lib/certification-format.ts","./packages/features/src/certifications/screens/certification-detail.tsx","./packages/features/src/certifications/screens/certification-edit.tsx","./packages/features/src/certifications/screens/certifications-list.tsx","./packages/features/src/chat/chat-identity.tsx","./packages/features/src/chat/index.ts","./packages/features/src/chat/nav.ts","./packages/features/src/chat/routes.ts","./packages/features/src/chat/__tests__/chat.test.tsx","./packages/features/src/chat/__tests__/chatbot.test.tsx","./packages/features/src/chat/__tests__/message-text.test.ts","./packages/features/src/chat/components/assistant-list-item.tsx","./packages/features/src/chat/components/channel-list-item.tsx","./packages/features/src/chat/components/chat-gate.tsx","./packages/features/src/chat/components/chat-image.tsx","./packages/features/src/chat/components/gif-picker-sheet.tsx","./packages/features/src/chat/components/message-actions-sheet.tsx","./packages/features/src/chat/components/message-bubble.tsx","./packages/features/src/chat/components/message-composer.tsx","./packages/features/src/chat/components/new-conversation-sheet.tsx","./packages/features/src/chat/components/typing-dots.tsx","./packages/features/src/chat/hooks/use-active-chat-channel.ts","./packages/features/src/chat/hooks/use-chat-availability.ts","./packages/features/src/chat/hooks/use-joined-chat-channel.ts","./packages/features/src/chat/lib/assistant-accent.ts","./packages/features/src/chat/lib/chat-actions.ts","./packages/features/src/chat/lib/chat-format.ts","./packages/features/src/chat/lib/message-text.ts","./packages/features/src/chat/screens/chat-channels.tsx","./packages/features/src/chat/screens/chat-conversation.tsx","./packages/features/src/chat/screens/chatbot.tsx","./packages/features/src/check-in/index.ts","./packages/features/src/check-in/__tests__/check-in.test.tsx","./packages/features/src/check-in/components/check-in-history-list.tsx","./packages/features/src/check-in/components/check-in-sheet.tsx","./packages/features/src/check-in/components/check-in-tab.tsx","./packages/features/src/check-in/components/check-in-timer-card.tsx","./packages/features/src/check-in/components/par-list.tsx","./packages/features/src/check-in/lib/check-in-status.ts","./packages/features/src/check-in/lib/check-in-target.ts","./packages/features/src/checklists/index.ts","./packages/features/src/checklists/nav.ts","./packages/features/src/checklists/routes.ts","./packages/features/src/checklists/runtime.ts","./packages/features/src/checklists/__tests__/checklists.test.tsx","./packages/features/src/checklists/components/checklist-calendar.tsx","./packages/features/src/checklists/components/checklist-list-controls.tsx","./packages/features/src/checklists/components/checklist-rows.tsx","./packages/features/src/checklists/components/checklist-run-sheet.tsx","./packages/features/src/checklists/components/checklist-tab-panel.tsx","./packages/features/src/checklists/components/due-checks-card.tsx","./packages/features/src/checklists/hooks/use-checklists.ts","./packages/features/src/checklists/lib/checklist-filters.ts","./packages/features/src/checklists/lib/checklist-form.ts","./packages/features/src/checklists/lib/checklists-api.ts","./packages/features/src/checklists/screens/checklists.tsx","./packages/features/src/contacts/index.ts","./packages/features/src/contacts/nav.ts","./packages/features/src/contacts/navigation-handoff.tsx","./packages/features/src/contacts/routes.ts","./packages/features/src/contacts/__tests__/contacts.test.tsx","./packages/features/src/contacts/components/contact-card.tsx","./packages/features/src/contacts/components/contact-field.tsx","./packages/features/src/contacts/components/contact-files-panel.tsx","./packages/features/src/contacts/components/contact-notes-list.tsx","./packages/features/src/contacts/components/contact-preplan-panel.tsx","./packages/features/src/contacts/lib/contact-format.ts","./packages/features/src/contacts/screens/contact-detail.tsx","./packages/features/src/contacts/screens/contacts-list.tsx","./packages/features/src/custom-maps/index.ts","./packages/features/src/custom-maps/nav.ts","./packages/features/src/custom-maps/routes.ts","./packages/features/src/custom-maps/__tests__/custom-maps.test.tsx","./packages/features/src/custom-maps/__tests__/test-setup.tsx","./packages/features/src/custom-maps/components/feature-detail-card.tsx","./packages/features/src/custom-maps/lib/map-viewer.ts","./packages/features/src/custom-maps/screens/custom-map.tsx","./packages/features/src/custom-maps/screens/indoor-map.tsx","./packages/features/src/custom-maps/screens/maps-home.tsx","./packages/features/src/custom-maps/screens/maps-search.tsx","./packages/features/src/dashboard/index.ts","./packages/features/src/dashboard/nav.ts","./packages/features/src/dashboard/routes.ts","./packages/features/src/dashboard/__tests__/board-data.test.ts","./packages/features/src/dashboard/__tests__/configure-screen.test.tsx","./packages/features/src/dashboard/__tests__/dashboard-screen.test.tsx","./packages/features/src/dashboard/__tests__/fixtures.ts","./packages/features/src/dashboard/__tests__/grid.test.ts","./packages/features/src/dashboard/__tests__/navigation-etas.test.tsx","./packages/features/src/dashboard/__tests__/openweather.test.ts","./packages/features/src/dashboard/__tests__/stores.test.ts","./packages/features/src/dashboard/__tests__/test-setup.tsx","./packages/features/src/dashboard/__tests__/unit-alerts.test.tsx","./packages/features/src/dashboard/__tests__/widget-settings.test.ts","./packages/features/src/dashboard/__tests__/widgets.test.tsx","./packages/features/src/dashboard/components/add-widget-menu.tsx","./packages/features/src/dashboard/components/dashboard-grid.tsx","./packages/features/src/dashboard/components/dashboard-header-actions.tsx","./packages/features/src/dashboard/components/widget-container.tsx","./packages/features/src/dashboard/components/widget-renderer.tsx","./packages/features/src/dashboard/components/widget-table.tsx","./packages/features/src/dashboard/components/configure/setting-controls.tsx","./packages/features/src/dashboard/components/configure/widget-settings-panel.tsx","./packages/features/src/dashboard/components/widgets/calls-summary-widget.tsx","./packages/features/src/dashboard/components/widgets/calls-widget.tsx","./packages/features/src/dashboard/components/widgets/map-widget.tsx","./packages/features/src/dashboard/components/widgets/notes-widget.tsx","./packages/features/src/dashboard/components/widgets/personnel-option-summary-widget.tsx","./packages/features/src/dashboard/components/widgets/personnel-widget.tsx","./packages/features/src/dashboard/components/widgets/scheduled-calls-widget.tsx","./packages/features/src/dashboard/components/widgets/summary-row.tsx","./packages/features/src/dashboard/components/widgets/time-widget.tsx","./packages/features/src/dashboard/components/widgets/types.ts","./packages/features/src/dashboard/components/widgets/unit-alerts-widget.tsx","./packages/features/src/dashboard/components/widgets/units-summary-widget.tsx","./packages/features/src/dashboard/components/widgets/units-widget.tsx","./packages/features/src/dashboard/components/widgets/weather-alerts-widget.tsx","./packages/features/src/dashboard/components/widgets/weather-widget.tsx","./packages/features/src/dashboard/hooks/use-dashboard-data.ts","./packages/features/src/dashboard/hooks/use-dashboard-store.ts","./packages/features/src/dashboard/hooks/use-dashboard-view-store.ts","./packages/features/src/dashboard/hooks/use-grid-config.ts","./packages/features/src/dashboard/hooks/use-priority-lookup.ts","./packages/features/src/dashboard/hooks/use-scheduled-call-dispatches.ts","./packages/features/src/dashboard/hooks/use-stale.ts","./packages/features/src/dashboard/hooks/use-widget-settings-store.ts","./packages/features/src/dashboard/hooks/use-widget-title.ts","./packages/features/src/dashboard/integrations/openweather/openweather-client.ts","./packages/features/src/dashboard/integrations/openweather/use-weather-outlook.ts","./packages/features/src/dashboard/lib/board-data.ts","./packages/features/src/dashboard/lib/column-labels.ts","./packages/features/src/dashboard/lib/format.ts","./packages/features/src/dashboard/lib/grid.ts","./packages/features/src/dashboard/lib/map-pins.ts","./packages/features/src/dashboard/lib/storage.ts","./packages/features/src/dashboard/lib/widget-settings.ts","./packages/features/src/dashboard/lib/widget-types.ts","./packages/features/src/dashboard/screens/configure-dashboard.tsx","./packages/features/src/dashboard/screens/dashboard.tsx","./packages/features/src/data-protection/index.ts","./packages/features/src/data-protection/nav.ts","./packages/features/src/data-protection/routes.ts","./packages/features/src/data-protection/__tests__/data-protection.test.tsx","./packages/features/src/data-protection/components/protected-reveal-bar.tsx","./packages/features/src/data-protection/components/protected-text.tsx","./packages/features/src/data-protection/components/step-up-prompt.tsx","./packages/features/src/data-protection/hooks/use-protected-reveal.ts","./packages/features/src/data-protection/lib/redaction.ts","./packages/features/src/department-config/index.ts","./packages/features/src/department-config/__tests__/department-config.test.ts","./packages/features/src/department-config/__tests__/department-time-zone.test.ts","./packages/features/src/department-config/hooks/use-department-config-store.ts","./packages/features/src/department-config/hooks/use-department-time-zone.ts","./packages/features/src/dispatch-console/index.ts","./packages/features/src/dispatch-console/nav.ts","./packages/features/src/dispatch-console/routes.ts","./packages/features/src/dispatch-console/__tests__/console-lib.test.ts","./packages/features/src/dispatch-console/__tests__/console-map-traffic.test.tsx","./packages/features/src/dispatch-console/__tests__/dispatch-console.test.tsx","./packages/features/src/dispatch-console/__tests__/panel-scrolling.test.ts","./packages/features/src/dispatch-console/components/actions-panel.tsx","./packages/features/src/dispatch-console/components/active-call-filter-banner.tsx","./packages/features/src/dispatch-console/components/active-calls-panel.tsx","./packages/features/src/dispatch-console/components/activity-panel.tsx","./packages/features/src/dispatch-console/components/add-note-sheet.tsx","./packages/features/src/dispatch-console/components/add-resources-sheet.tsx","./packages/features/src/dispatch-console/components/console-call-card.tsx","./packages/features/src/dispatch-console/components/console-map.tsx","./packages/features/src/dispatch-console/components/console-status-sheets.tsx","./packages/features/src/dispatch-console/components/map-panel.tsx","./packages/features/src/dispatch-console/components/notes-panel.tsx","./packages/features/src/dispatch-console/components/panel-frame.tsx","./packages/features/src/dispatch-console/components/panel-search.tsx","./packages/features/src/dispatch-console/components/personnel-panel.tsx","./packages/features/src/dispatch-console/components/ptt-panel.tsx","./packages/features/src/dispatch-console/components/resource-rows.tsx","./packages/features/src/dispatch-console/components/resources-panel.tsx","./packages/features/src/dispatch-console/components/stats-header.tsx","./packages/features/src/dispatch-console/components/unit-timer-activity.tsx","./packages/features/src/dispatch-console/components/units-panel.tsx","./packages/features/src/dispatch-console/components/view-toggles.tsx","./packages/features/src/dispatch-console/hooks/use-console-data.ts","./packages/features/src/dispatch-console/hooks/use-console-store.ts","./packages/features/src/dispatch-console/hooks/use-dashboard-view-store.ts","./packages/features/src/dispatch-console/hooks/use-pending-resource-ids.ts","./packages/features/src/dispatch-console/hooks/use-radio-log.ts","./packages/features/src/dispatch-console/hooks/use-selected-call.ts","./packages/features/src/dispatch-console/lib/bulk-status.ts","./packages/features/src/dispatch-console/lib/console-filters.ts","./packages/features/src/dispatch-console/lib/console-layout.ts","./packages/features/src/dispatch-console/lib/console-stats.ts","./packages/features/src/dispatch-console/lib/console-types.ts","./packages/features/src/dispatch-console/screens/dispatch-console.tsx","./packages/features/src/incident-command/index.ts","./packages/features/src/incident-command/nav.ts","./packages/features/src/incident-command/routes.ts","./packages/features/src/incident-command/__tests__/board-layout.test.ts","./packages/features/src/incident-command/__tests__/command-board-view.test.tsx","./packages/features/src/incident-command/__tests__/fixtures.ts","./packages/features/src/incident-command/__tests__/incident-command-lib.test.ts","./packages/features/src/incident-command/__tests__/incident-command-screens.test.tsx","./packages/features/src/incident-command/__tests__/incident-command-sections.test.tsx","./packages/features/src/incident-command/__tests__/incident-command-tab.test.tsx","./packages/features/src/incident-command/__tests__/test-setup.tsx","./packages/features/src/incident-command/components/accountability-section.tsx","./packages/features/src/incident-command/components/board-buttons.tsx","./packages/features/src/incident-command/components/command-board-view.tsx","./packages/features/src/incident-command/components/command-header.tsx","./packages/features/src/incident-command/components/incident-card.tsx","./packages/features/src/incident-command/components/incident-chat-section.tsx","./packages/features/src/incident-command/components/incident-command-tab.tsx","./packages/features/src/incident-command/components/log-section.tsx","./packages/features/src/incident-command/components/needs-section.tsx","./packages/features/src/incident-command/components/notes-section.tsx","./packages/features/src/incident-command/components/objectives-section.tsx","./packages/features/src/incident-command/components/percent-bar.tsx","./packages/features/src/incident-command/components/resource-incident-panel.tsx","./packages/features/src/incident-command/components/roles-section.tsx","./packages/features/src/incident-command/components/section-card.tsx","./packages/features/src/incident-command/components/sort.ts","./packages/features/src/incident-command/components/structure-board.tsx","./packages/features/src/incident-command/components/timers-section.tsx","./packages/features/src/incident-command/components/voice-section.tsx","./packages/features/src/incident-command/components/work-time-light.tsx","./packages/features/src/incident-command/components/sheets/ad-hoc-resource-sheet.tsx","./packages/features/src/incident-command/components/sheets/assign-resource-sheet.tsx","./packages/features/src/incident-command/components/sheets/assistant-sheet.tsx","./packages/features/src/incident-command/components/sheets/choice-chips.tsx","./packages/features/src/incident-command/components/sheets/command-sheets.tsx","./packages/features/src/incident-command/components/sheets/lane-sheet.tsx","./packages/features/src/incident-command/components/sheets/lifecycle-sheets.tsx","./packages/features/src/incident-command/components/sheets/need-sheets.tsx","./packages/features/src/incident-command/components/sheets/objective-sheets.tsx","./packages/features/src/incident-command/components/sheets/person-picker.tsx","./packages/features/src/incident-command/components/sheets/resource-sheet.tsx","./packages/features/src/incident-command/components/sheets/sheet-form.tsx","./packages/features/src/incident-command/hooks/resource-position.ts","./packages/features/src/incident-command/hooks/use-board-roster.ts","./packages/features/src/incident-command/hooks/use-command-actions.ts","./packages/features/src/incident-command/hooks/use-command-permissions.ts","./packages/features/src/incident-command/hooks/use-incident-chat.ts","./packages/features/src/incident-command/hooks/use-is-online.ts","./packages/features/src/incident-command/hooks/use-now.ts","./packages/features/src/incident-command/lib/board-layout.ts","./packages/features/src/incident-command/lib/board.ts","./packages/features/src/incident-command/lib/events.ts","./packages/features/src/incident-command/lib/labels.ts","./packages/features/src/incident-command/lib/map-features.ts","./packages/features/src/incident-command/lib/permissions.ts","./packages/features/src/incident-command/screens/call-command.tsx","./packages/features/src/incident-command/screens/command-board.tsx","./packages/features/src/incident-command/screens/command-log.tsx","./packages/features/src/incident-command/screens/command-map.tsx","./packages/features/src/incident-command/screens/incidents-list.tsx","./packages/features/src/incident-command/screens/resource-map.tsx","./packages/features/src/inventory/index.ts","./packages/features/src/inventory/nav.ts","./packages/features/src/inventory/routes.ts","./packages/features/src/inventory/__tests__/inventory.test.tsx","./packages/features/src/inventory/components/confirm-dialog.tsx","./packages/features/src/inventory/components/count-line-row.tsx","./packages/features/src/inventory/lib/count.ts","./packages/features/src/inventory/screens/inventory-count.tsx","./packages/features/src/inventory/screens/inventory-home.tsx","./packages/features/src/map/index.ts","./packages/features/src/map/nav.ts","./packages/features/src/map/routes.ts","./packages/features/src/map/__tests__/fixtures.ts","./packages/features/src/map/__tests__/live-map-pins-eta.test.tsx","./packages/features/src/map/__tests__/map-lib.test.ts","./packages/features/src/map/__tests__/map-lock.test.tsx","./packages/features/src/map/__tests__/map-pin-details.test.tsx","./packages/features/src/map/__tests__/map-screen.test.tsx","./packages/features/src/map/__tests__/map-traffic-route.test.tsx","./packages/features/src/map/__tests__/test-setup.tsx","./packages/features/src/map/components/active-route-overlay.tsx","./packages/features/src/map/components/map-highlights.tsx","./packages/features/src/map/components/map-pin-details.tsx","./packages/features/src/map/hooks/use-active-route-overlay.ts","./packages/features/src/map/hooks/use-department-layers.ts","./packages/features/src/map/hooks/use-live-map-pins.ts","./packages/features/src/map/hooks/use-map-lock.ts","./packages/features/src/map/hooks/use-map-view-store.ts","./packages/features/src/map/hooks/use-traffic-preference.ts","./packages/features/src/map/lib/layer-geojson.ts","./packages/features/src/map/lib/map-camera.ts","./packages/features/src/map/lib/map-markers.ts","./packages/features/src/map/screens/map-screen.tsx","./packages/features/src/member-home/hook-points.ts","./packages/features/src/member-home/index.ts","./packages/features/src/member-home/nav.ts","./packages/features/src/member-home/routes.ts","./packages/features/src/member-home/__tests__/member-home.test.tsx","./packages/features/src/member-home/components/active-call-tab.tsx","./packages/features/src/member-home/components/home-status-buttons.tsx","./packages/features/src/member-home/components/member-home-tabs-layout.tsx","./packages/features/src/member-home/components/summary-stats-row.tsx","./packages/features/src/member-home/components/user-status-card.tsx","./packages/features/src/member-home/hooks/use-own-status.ts","./packages/features/src/member-home/lib/home-statuses.ts","./packages/features/src/member-home/lib/summary-counts.ts","./packages/features/src/member-home/screens/member-home.tsx","./packages/features/src/messages/index.ts","./packages/features/src/messages/nav.ts","./packages/features/src/messages/routes.ts","./packages/features/src/messages/__tests__/messages.test.tsx","./packages/features/src/messages/components/confirm-sheet.tsx","./packages/features/src/messages/components/message-card.tsx","./packages/features/src/messages/components/message-details.tsx","./packages/features/src/messages/components/queued-message-row.tsx","./packages/features/src/messages/lib/message-format.ts","./packages/features/src/messages/screens/message-compose.tsx","./packages/features/src/messages/screens/message-detail.tsx","./packages/features/src/messages/screens/messages.tsx","./packages/features/src/navigation/index.ts","./packages/features/src/navigation/nav.ts","./packages/features/src/navigation/routes.ts","./packages/features/src/navigation/__tests__/navigation-screen.test.tsx","./packages/features/src/navigation/__tests__/test-setup.tsx","./packages/features/src/navigation/components/arrival-panels.tsx","./packages/features/src/navigation/hooks/use-in-app-navigation.ts","./packages/features/src/navigation/screens/navigation-screen.tsx","./packages/features/src/navigation-eta/index.ts","./packages/features/src/navigation-eta/__tests__/navigation-eta.test.tsx","./packages/features/src/navigation-eta/components/navigation-eta-chip.tsx","./packages/features/src/navigation-eta/hooks/use-navigation-eta.ts","./packages/features/src/navigation-eta/lib/eta-pins.ts","./packages/features/src/navigation-eta/lib/eta-text.ts","./packages/features/src/notes/index.ts","./packages/features/src/notes/nav.ts","./packages/features/src/notes/routes.ts","./packages/features/src/notes/__tests__/notes.test.tsx","./packages/features/src/notes/components/note-card.tsx","./packages/features/src/notes/screens/note-detail.tsx","./packages/features/src/notes/screens/note-list.tsx","./packages/features/src/notifications/index.ts","./packages/features/src/notifications/nav.ts","./packages/features/src/notifications/routes.ts","./packages/features/src/notifications/__tests__/inbox-selection.test.ts","./packages/features/src/notifications/__tests__/notifications.test.tsx","./packages/features/src/notifications/components/notification-detail-sheet.tsx","./packages/features/src/notifications/components/notification-row.tsx","./packages/features/src/notifications/components/push-notification-modal.tsx","./packages/features/src/notifications/lib/inbox-item.ts","./packages/features/src/notifications/lib/inbox-selection.ts","./packages/features/src/notifications/screens/notifications-inbox.tsx","./packages/features/src/offline-maps/index.ts","./packages/features/src/offline-maps/nav.ts","./packages/features/src/offline-maps/routes.ts","./packages/features/src/offline-maps/runtime.ts","./packages/features/src/offline-maps/settings-section.tsx","./packages/features/src/offline-maps/__tests__/fake-controller.ts","./packages/features/src/offline-maps/__tests__/offline-maps-screen.test.tsx","./packages/features/src/offline-maps/__tests__/settings-section.test.tsx","./packages/features/src/offline-maps/hooks/use-offline-maps.ts","./packages/features/src/offline-maps/lib/present.ts","./packages/features/src/offline-maps/screens/offline-maps-screen.tsx","./packages/features/src/operations/index.ts","./packages/features/src/operations/nav.ts","./packages/features/src/operations/routes.ts","./packages/features/src/operations/__tests__/operations.test.tsx","./packages/features/src/operations/components/deployment-card.tsx","./packages/features/src/operations/components/expenses-panel.tsx","./packages/features/src/operations/components/mars-panel.tsx","./packages/features/src/operations/components/operations-layout.tsx","./packages/features/src/operations/components/scope-picker.tsx","./packages/features/src/operations/components/time-report-editor.tsx","./packages/features/src/operations/components/usage-form.tsx","./packages/features/src/operations/hooks/use-time-workspace.ts","./packages/features/src/operations/lib/operations-capabilities.ts","./packages/features/src/operations/lib/time.ts","./packages/features/src/operations/screens/operations-deployment.tsx","./packages/features/src/operations/screens/operations-list.tsx","./packages/features/src/personnel/index.ts","./packages/features/src/personnel/nav.ts","./packages/features/src/personnel/routes.ts","./packages/features/src/personnel/__tests__/units-personnel.test.tsx","./packages/features/src/personnel/components/personnel-card.tsx","./packages/features/src/personnel/hooks/use-personnel-view-store.ts","./packages/features/src/personnel/lib/personnel-filters.ts","./packages/features/src/personnel/screens/personnel-detail.tsx","./packages/features/src/personnel/screens/personnel-list.tsx","./packages/features/src/pois/index.ts","./packages/features/src/pois/nav.ts","./packages/features/src/pois/routes.ts","./packages/features/src/pois/__tests__/pois.test.tsx","./packages/features/src/pois/components/poi-card.tsx","./packages/features/src/pois/components/poi-filter-bar.tsx","./packages/features/src/pois/lib/poi-filters.ts","./packages/features/src/pois/screens/poi-detail.tsx","./packages/features/src/pois/screens/pois-list.tsx","./packages/features/src/protocols/index.ts","./packages/features/src/protocols/nav.ts","./packages/features/src/protocols/routes.ts","./packages/features/src/protocols/__tests__/protocols.test.tsx","./packages/features/src/protocols/components/protocol-card.tsx","./packages/features/src/protocols/components/protocol-content.tsx","./packages/features/src/protocols/screens/protocol-detail.tsx","./packages/features/src/protocols/screens/protocol-list.tsx","./packages/features/src/records/index.ts","./packages/features/src/records/nav.ts","./packages/features/src/records/routes.ts","./packages/features/src/records/runtime.ts","./packages/features/src/records/__tests__/records-lib.test.ts","./packages/features/src/records/__tests__/records-screens.test.tsx","./packages/features/src/records/components/call-records-tab.tsx","./packages/features/src/records/components/deployment-items.tsx","./packages/features/src/records/components/record-attachments.tsx","./packages/features/src/records/components/record-field.tsx","./packages/features/src/records/components/record-form.tsx","./packages/features/src/records/components/record-list-item.tsx","./packages/features/src/records/components/records-layout.tsx","./packages/features/src/records/components/records-quick-create.tsx","./packages/features/src/records/hooks/use-department-reconciliation.ts","./packages/features/src/records/hooks/use-record.ts","./packages/features/src/records/hooks/use-records-context.ts","./packages/features/src/records/hooks/use-records-field-status.ts","./packages/features/src/records/lib/definition-name.ts","./packages/features/src/records/lib/deployment-format.ts","./packages/features/src/records/lib/drafts.ts","./packages/features/src/records/lib/record-schema.ts","./packages/features/src/records/lib/records-api.ts","./packages/features/src/records/screens/record-connector-detail.tsx","./packages/features/src/records/screens/record-connectors.tsx","./packages/features/src/records/screens/record-deployment-detail.tsx","./packages/features/src/records/screens/record-deployments.tsx","./packages/features/src/records/screens/record-detail.tsx","./packages/features/src/records/screens/record-new.tsx","./packages/features/src/records/screens/records-home.tsx","./packages/features/src/roles/index.ts","./packages/features/src/roles/nav.ts","./packages/features/src/roles/routes.ts","./packages/features/src/roles/__tests__/unit-roles.test.tsx","./packages/features/src/roles/components/role-assignment-item.tsx","./packages/features/src/roles/components/role-user-selection-modal.tsx","./packages/features/src/roles/components/unit-roles-button.tsx","./packages/features/src/roles/components/unit-roles-sheet.tsx","./packages/features/src/roles/lib/role-assignments.ts","./packages/features/src/runcards/index.ts","./packages/features/src/runcards/nav.ts","./packages/features/src/runcards/register.tsx","./packages/features/src/runcards/routes.ts","./packages/features/src/runcards/__tests__/run-card-lib.test.ts","./packages/features/src/runcards/__tests__/runcards-components.test.tsx","./packages/features/src/runcards/components/add-resources-recommendation.tsx","./packages/features/src/runcards/components/alarm-level-badge.tsx","./packages/features/src/runcards/components/escalate-alarm-button.tsx","./packages/features/src/runcards/components/recommendation-panel.tsx","./packages/features/src/runcards/components/response-code-chip.tsx","./packages/features/src/runcards/components/run-card-call-section.tsx","./packages/features/src/runcards/components/run-card-decision-section.tsx","./packages/features/src/runcards/components/run-card-recommendation.tsx","./packages/features/src/runcards/hooks/use-add-resources-recommendation.ts","./packages/features/src/runcards/hooks/use-call-recommendation.ts","./packages/features/src/runcards/hooks/use-run-card-flags.ts","./packages/features/src/runcards/lib/recommendation-types.ts","./packages/features/src/runcards/lib/run-card-conditions.ts","./packages/features/src/runcards/lib/run-cards.ts","./packages/features/src/settings/index.ts","./packages/features/src/settings/nav.ts","./packages/features/src/settings/registry.ts","./packages/features/src/settings/routes.ts","./packages/features/src/settings/runtime.ts","./packages/features/src/settings/__tests__/app-updates-section.test.tsx","./packages/features/src/settings/__tests__/native-mocks.ts","./packages/features/src/settings/__tests__/navigation-and-preferences.test.ts","./packages/features/src/settings/__tests__/settings-screen.test.tsx","./packages/features/src/settings/__tests__/storage-sync-screen.test.tsx","./packages/features/src/settings/__tests__/test-setup.ts","./packages/features/src/settings/__tests__/variant.ts","./packages/features/src/settings/__tests__/voice-section.test.tsx","./packages/features/src/settings/components/settings-row.tsx","./packages/features/src/settings/hooks/use-storage-sync.ts","./packages/features/src/settings/lib/format.ts","./packages/features/src/settings/lib/navigation-app.ts","./packages/features/src/settings/lib/navigation-handoff.ts","./packages/features/src/settings/lib/preferences.ts","./packages/features/src/settings/screens/settings-screen.tsx","./packages/features/src/settings/screens/storage-sync-screen.tsx","./packages/features/src/settings/sections/account-section.tsx","./packages/features/src/settings/sections/app-updates-section.tsx","./packages/features/src/settings/sections/built-in.tsx","./packages/features/src/settings/sections/location-section.tsx","./packages/features/src/settings/sections/notifications-section.tsx","./packages/features/src/settings/sections/other-sections.tsx","./packages/features/src/settings/sections/preferences-section.tsx","./packages/features/src/settings/sections/unit-section.tsx","./packages/features/src/settings/sections/voice-section.tsx","./packages/features/src/shifts/index.ts","./packages/features/src/shifts/nav.ts","./packages/features/src/shifts/routes.ts","./packages/features/src/shifts/__tests__/fixtures.ts","./packages/features/src/shifts/__tests__/shift-lib.test.ts","./packages/features/src/shifts/__tests__/shifts.test.tsx","./packages/features/src/shifts/__tests__/source-rules.test.ts","./packages/features/src/shifts/components/shift-action-sheet.tsx","./packages/features/src/shifts/components/shift-calendar-view.tsx","./packages/features/src/shifts/components/shift-card.tsx","./packages/features/src/shifts/components/shift-day-card.tsx","./packages/features/src/shifts/components/shift-day-details.tsx","./packages/features/src/shifts/components/shift-lists.tsx","./packages/features/src/shifts/components/shift-select-row.tsx","./packages/features/src/shifts/components/shift-sheets.tsx","./packages/features/src/shifts/components/shift-trade-card.tsx","./packages/features/src/shifts/components/shift-trade-details.tsx","./packages/features/src/shifts/hooks/use-listed-shift-day.ts","./packages/features/src/shifts/hooks/use-rerender-at.ts","./packages/features/src/shifts/hooks/use-shift-action.ts","./packages/features/src/shifts/hooks/use-shift-day-lists.ts","./packages/features/src/shifts/hooks/use-shift-day-over.ts","./packages/features/src/shifts/lib/shift-action-types.ts","./packages/features/src/shifts/lib/shift-loaders.ts","./packages/features/src/shifts/lib/shift-rules.ts","./packages/features/src/shifts/lib/shift-time.ts","./packages/features/src/shifts/screens/shift-day.tsx","./packages/features/src/shifts/screens/shift-trade.tsx","./packages/features/src/shifts/screens/shifts.tsx","./packages/features/src/staffing/index.ts","./packages/features/src/staffing/nav.ts","./packages/features/src/staffing/routes.ts","./packages/features/src/staffing/__tests__/staffing.test.tsx","./packages/features/src/staffing/components/member-state-card.tsx","./packages/features/src/staffing/components/staffing-buttons.tsx","./packages/features/src/staffing/components/staffing-note-sheet.tsx","./packages/features/src/staffing/components/user-staffing-card.tsx","./packages/features/src/staffing/hooks/use-own-staffing.ts","./packages/features/src/staffing/hooks/use-staffing-actions.ts","./packages/features/src/staffing/lib/label-colors.ts","./packages/features/src/status/index.ts","./packages/features/src/status/nav.ts","./packages/features/src/status/routes.ts","./packages/features/src/status/status-context.tsx","./packages/features/src/status/__tests__/status-lib.test.ts","./packages/features/src/status/__tests__/status-picker.test.tsx","./packages/features/src/status/__tests__/status-sheets.test.tsx","./packages/features/src/status/__tests__/test-setup.tsx","./packages/features/src/status/components/availability-filter-chips.tsx","./packages/features/src/status/components/current-status-badge.tsx","./packages/features/src/status/components/hold-to-confirm-button.tsx","./packages/features/src/status/components/personnel-status-sheet.tsx","./packages/features/src/status/components/status-destination-list.tsx","./packages/features/src/status/components/status-option.tsx","./packages/features/src/status/components/status-picker.tsx","./packages/features/src/status/components/status-sheet.tsx","./packages/features/src/status/components/unit-status-sheet.tsx","./packages/features/src/status/lib/base-types.ts","./packages/features/src/status/lib/location-for-status.ts","./packages/features/src/status/lib/readable-colors.ts","./packages/features/src/status/lib/status-destination.ts","./packages/features/src/status/lib/status-flow.ts","./packages/features/src/status/lib/status-submit-error.ts","./packages/features/src/status/lib/unit-status-set.ts","./packages/features/src/unit-routes/index.ts","./packages/features/src/unit-routes/map-slot.tsx","./packages/features/src/unit-routes/nav.ts","./packages/features/src/unit-routes/routes.ts","./packages/features/src/unit-routes/__tests__/fixtures.ts","./packages/features/src/unit-routes/__tests__/route-lib.test.ts","./packages/features/src/unit-routes/__tests__/routes-screens.test.tsx","./packages/features/src/unit-routes/components/confirm-dialog.tsx","./packages/features/src/unit-routes/components/deviation-banner.tsx","./packages/features/src/unit-routes/components/route-card.tsx","./packages/features/src/unit-routes/components/route-plans-list.tsx","./packages/features/src/unit-routes/components/route-stats.tsx","./packages/features/src/unit-routes/components/skip-stop-dialog.tsx","./packages/features/src/unit-routes/components/stop-card.tsx","./packages/features/src/unit-routes/hooks/use-stop-actions.ts","./packages/features/src/unit-routes/lib/route-format.ts","./packages/features/src/unit-routes/lib/route-geo.ts","./packages/features/src/unit-routes/lib/route-map.ts","./packages/features/src/unit-routes/lib/route-status.ts","./packages/features/src/unit-routes/screens/route-active.tsx","./packages/features/src/unit-routes/screens/route-directions.tsx","./packages/features/src/unit-routes/screens/route-history.tsx","./packages/features/src/unit-routes/screens/route-instance.tsx","./packages/features/src/unit-routes/screens/route-poi.tsx","./packages/features/src/unit-routes/screens/route-start.tsx","./packages/features/src/unit-routes/screens/route-stop-contact.tsx","./packages/features/src/unit-routes/screens/route-stop.tsx","./packages/features/src/unit-routes/screens/routes-home.tsx","./packages/features/src/unit-status/index.ts","./packages/features/src/unit-status/nav.ts","./packages/features/src/unit-status/register.tsx","./packages/features/src/unit-status/routes.ts","./packages/features/src/unit-status/__tests__/active-call-actions.test.tsx","./packages/features/src/unit-status/__tests__/status-timer-notifications.test.tsx","./packages/features/src/unit-status/__tests__/status-timer.test.tsx","./packages/features/src/unit-status/__tests__/test-helpers.ts","./packages/features/src/unit-status/__tests__/test-setup.tsx","./packages/features/src/unit-status/__tests__/unit-status-console.test.tsx","./packages/features/src/unit-status/__tests__/unit-status-lib.test.ts","./packages/features/src/unit-status/components/active-call-card.tsx","./packages/features/src/unit-status/components/active-call-sheet.tsx","./packages/features/src/unit-status/components/check-in-summary.tsx","./packages/features/src/unit-status/components/confirm-dialog.tsx","./packages/features/src/unit-status/components/current-status-card.tsx","./packages/features/src/unit-status/components/current-status-tile.tsx","./packages/features/src/unit-status/components/quick-actions.tsx","./packages/features/src/unit-status/components/set-active-call-button.tsx","./packages/features/src/unit-status/components/shared-session-card.tsx","./packages/features/src/unit-status/components/status-buttons.tsx","./packages/features/src/unit-status/components/status-timer-banner.tsx","./packages/features/src/unit-status/components/status-timer-card.tsx","./packages/features/src/unit-status/components/status-timer-notifications.tsx","./packages/features/src/unit-status/components/unit-status-panel.tsx","./packages/features/src/unit-status/components/unit-status-sidebar.tsx","./packages/features/src/unit-status/components/unit-summary-card.tsx","./packages/features/src/unit-status/hooks/use-active-unit.ts","./packages/features/src/unit-status/lib/call-location.ts","./packages/features/src/unit-status/lib/console-layout.ts","./packages/features/src/unit-status/lib/quick-actions.ts","./packages/features/src/unit-status/lib/status-display.ts","./packages/features/src/unit-status/lib/status-timer-text.ts","./packages/features/src/unit-status/screens/unit-console.tsx","./packages/features/src/unit-status-alerts/index.ts","./packages/features/src/unit-status-alerts/__tests__/unit-status-alerts-lib.test.ts","./packages/features/src/unit-status-alerts/__tests__/unit-status-timer-schedule.test.ts","./packages/features/src/unit-status-alerts/components/unit-alert-acknowledge-sheet.tsx","./packages/features/src/unit-status-alerts/components/unit-alert-badge.tsx","./packages/features/src/unit-status-alerts/components/unit-alert-list.tsx","./packages/features/src/unit-status-alerts/hooks/use-unit-alerts-sync.ts","./packages/features/src/unit-status-alerts/hooks/use-unit-alerts.ts","./packages/features/src/unit-status-alerts/hooks/use-unit-status-timer.ts","./packages/features/src/unit-status-alerts/lib/unit-status-alerts.ts","./packages/features/src/units/index.ts","./packages/features/src/units/nav.ts","./packages/features/src/units/routes.ts","./packages/features/src/units/components/filter-options-sheet.tsx","./packages/features/src/units/components/unit-card.tsx","./packages/features/src/units/hooks/use-units-view-store.ts","./packages/features/src/units/lib/unit-filters.ts","./packages/features/src/units/screens/unit-detail.tsx","./packages/features/src/units/screens/units-list.tsx","./packages/features/src/video-feeds/index.ts","./packages/features/src/video-feeds/__tests__/video-feeds.test.tsx","./packages/features/src/video-feeds/components/video-feed-card.tsx","./packages/features/src/video-feeds/components/video-feed-form-sheet.tsx","./packages/features/src/video-feeds/components/video-feed-player.tsx","./packages/features/src/video-feeds/components/video-feed-surface.tsx","./packages/features/src/video-feeds/components/video-feed-surface.web.tsx","./packages/features/src/video-feeds/components/video-feeds-tab.tsx","./packages/features/src/video-feeds/lib/feed-format.ts","./packages/features/src/voice/index.ts","./packages/features/src/voice/__tests__/voice-sheets.test.tsx","./packages/features/src/voice/components/audio-stream-sheet.tsx","./packages/features/src/voice/components/voice-channel-sheet.tsx","./packages/features/src/voice/components/voice-menu-buttons.tsx","./packages/features/src/voice/lib/open-voice-sheets.ts","./packages/features/src/weather-alerts/index.ts","./packages/features/src/weather-alerts/nav.ts","./packages/features/src/weather-alerts/routes.ts","./packages/features/src/weather-alerts/__tests__/weather-alerts.test.tsx","./packages/features/src/weather-alerts/components/severity-filter-tabs.tsx","./packages/features/src/weather-alerts/components/weather-alert-banner.tsx","./packages/features/src/weather-alerts/components/weather-alert-card.tsx","./packages/features/src/weather-alerts/components/weather-alert-source-sheet.tsx","./packages/features/src/weather-alerts/components/weather-alert-zone-sheet.tsx","./packages/features/src/weather-alerts/hooks/use-weather-alerts-realtime.ts","./packages/features/src/weather-alerts/lib/weather-alert-format.ts","./packages/features/src/weather-alerts/screens/weather-alert-detail.tsx","./packages/features/src/weather-alerts/screens/weather-alert-settings.tsx","./packages/features/src/weather-alerts/screens/weather-alerts-list.tsx","./packages/features/src/work-orders/index.ts","./packages/features/src/work-orders/nav.ts","./packages/features/src/work-orders/routes.ts","./packages/features/src/work-orders/__tests__/work-orders.test.tsx","./packages/features/src/work-orders/components/choice-chips.tsx","./packages/features/src/work-orders/components/work-order-files.tsx","./packages/features/src/work-orders/components/work-order-filter-sheet.tsx","./packages/features/src/work-orders/components/work-order-row.tsx","./packages/features/src/work-orders/components/work-order-transition-panel.tsx","./packages/features/src/work-orders/components/work-orders-gate.tsx","./packages/features/src/work-orders/lib/work-order-filters.ts","./packages/features/src/work-orders/lib/work-order-rules.ts","./packages/features/src/work-orders/screens/work-order-detail.tsx","./packages/features/src/work-orders/screens/work-order-report.tsx","./packages/features/src/work-orders/screens/work-orders.tsx","./packages/i18n/src/detect.ts","./packages/i18n/src/i18n.ts","./packages/i18n/src/index.ts","./packages/i18n/src/languages.ts","./packages/i18n/src/namespaces.ts","./packages/i18n/src/__tests__/detect.test.ts","./packages/i18n/src/__tests__/i18n.test.ts","./packages/i18n/src/__tests__/merge-script.test.ts","./packages/i18n/src/__tests__/translations.test.ts","./packages/i18n/src/resources/all.ts","./packages/i18n/src/resources/bigboard.ts","./packages/i18n/src/resources/common.ts","./packages/i18n/src/resources/dispatch.ts","./packages/i18n/src/resources/ic.ts","./packages/i18n/src/resources/responder.ts","./packages/i18n/src/resources/types.ts","./packages/i18n/src/resources/unit.ts","./packages/location/src/background-task.ts","./packages/location/src/configure.ts","./packages/location/src/constants.ts","./packages/location/src/geo.ts","./packages/location/src/i18n-keys.ts","./packages/location/src/index.ts","./packages/location/src/live-locations.ts","./packages/location/src/location-fix.ts","./packages/location/src/location-store.ts","./packages/location/src/logger.ts","./packages/location/src/observable.ts","./packages/location/src/outbox-handlers.ts","./packages/location/src/payload.ts","./packages/location/src/permissions.ts","./packages/location/src/settings-dialog.ts","./packages/location/src/task-options.ts","./packages/location/src/task-runtime.ts","./packages/location/src/timeouts.ts","./packages/location/src/transmission-gate.ts","./packages/location/src/transmitter.ts","./packages/location/src/types.ts","./packages/location/src/web-geolocation.ts","./packages/location/src/__tests__/background-task.test.ts","./packages/location/src/__tests__/configure-and-hooks.test.ts","./packages/location/src/__tests__/helpers.ts","./packages/location/src/__tests__/location-store.test.ts","./packages/location/src/__tests__/offline-queue.test.ts","./packages/location/src/__tests__/outbox-handlers.test.ts","./packages/location/src/__tests__/payload.test.ts","./packages/location/src/__tests__/permissions-and-fix.test.ts","./packages/location/src/__tests__/transmission-gate.test.ts","./packages/location/src/__tests__/transmitter.test.ts","./packages/location/src/__tests__/use-map-geolocation-updates.test.ts","./packages/location/src/__tests__/fakes/expo-constants.ts","./packages/location/src/__tests__/fakes/expo-localization.ts","./packages/location/src/__tests__/fakes/expo-location.ts","./packages/location/src/__tests__/fakes/expo-task-manager.ts","./packages/location/src/__tests__/fakes/react-native.ts","./packages/location/src/hooks/use-current-location.ts","./packages/location/src/hooks/use-location-transmission-status.ts","./packages/location/src/hooks/use-map-geolocation-updates.ts","./packages/maps/src/index.ts","./packages/maps/src/types.ts","./packages/maps/src/__tests__/components.test.tsx","./packages/maps/src/__tests__/lib.test.ts","./packages/maps/src/__tests__/map-provider.test.tsx","./packages/maps/src/__tests__/maplibre-loader.web.test.ts","./packages/maps/src/__tests__/offline-badge.test.tsx","./packages/maps/src/__tests__/offline-department-data.test.tsx","./packages/maps/src/__tests__/offline-switch.web.test.tsx","./packages/maps/src/__tests__/pmtiles-local-packs.web.test.ts","./packages/maps/src/__tests__/pmtiles.web.test.ts","./packages/maps/src/__tests__/rg-map-lazy.web.test.tsx","./packages/maps/src/__tests__/rg-map-retry.web.test.tsx","./packages/maps/src/__tests__/rg-map.native.test.tsx","./packages/maps/src/__tests__/rg-map.web.test.tsx","./packages/maps/src/__tests__/signed-layers.test.tsx","./packages/maps/src/__tests__/style-resolution.test.ts","./packages/maps/src/__tests__/traffic-layer.web.test.tsx","./packages/maps/src/__tests__/traffic.test.tsx","./packages/maps/src/__tests__/url-signer.test.ts","./packages/maps/src/__tests__/fakes/maplibre-gl.ts","./packages/maps/src/__tests__/fakes/maplibre-react-native.tsx","./packages/maps/src/__tests__/fakes/pmtiles.ts","./packages/maps/src/__tests__/fakes/resgrid-core.ts","./packages/maps/src/__tests__/fakes/style-stub.ts","./packages/maps/src/components/department-map-layers.tsx","./packages/maps/src/components/full-screen-location-picker.tsx","./packages/maps/src/components/full-screen-map.tsx","./packages/maps/src/components/location-picker.tsx","./packages/maps/src/components/map-banners.tsx","./packages/maps/src/components/map-controls.tsx","./packages/maps/src/components/map-legend-sheet.tsx","./packages/maps/src/components/map-pins.tsx","./packages/maps/src/components/offline-department-layers.tsx","./packages/maps/src/components/pin-detail-sheet.tsx","./packages/maps/src/components/pin-marker.tsx","./packages/maps/src/components/pixel-image-map.tsx","./packages/maps/src/components/poi-list.tsx","./packages/maps/src/components/poi-markers.tsx","./packages/maps/src/components/sheet-parts.tsx","./packages/maps/src/components/static-map.tsx","./packages/maps/src/components/unit-location-marker.tsx","./packages/maps/src/hooks/use-camera-helpers.ts","./packages/maps/src/hooks/use-local-file-url.ts","./packages/maps/src/hooks/use-local-file-url.web.ts","./packages/maps/src/hooks/use-signed-map-url.ts","./packages/maps/src/lib/camera.ts","./packages/maps/src/lib/custom-maps.ts","./packages/maps/src/lib/offline-style-sources.ts","./packages/maps/src/lib/pin-features.ts","./packages/maps/src/lib/pin-ids.ts","./packages/maps/src/lib/pixel-frame.ts","./packages/maps/src/lib/poi.ts","./packages/maps/src/lib/static-map.ts","./packages/maps/src/offline/ambient-cache.ts","./packages/maps/src/offline/ambient-cache.web.ts","./packages/maps/src/offline/background-transfer.ts","./packages/maps/src/offline/department-data.ts","./packages/maps/src/offline/downloader.ts","./packages/maps/src/offline/errors.ts","./packages/maps/src/offline/expo-background-transfer.ts","./packages/maps/src/offline/expo-file-store.ts","./packages/maps/src/offline/expo-file-store.web.ts","./packages/maps/src/offline/file-store.ts","./packages/maps/src/offline/glyphs.ts","./packages/maps/src/offline/hydration.ts","./packages/maps/src/offline/index.ts","./packages/maps/src/offline/manager.ts","./packages/maps/src/offline/manifest-client.ts","./packages/maps/src/offline/manifest.ts","./packages/maps/src/offline/mode-switch.ts","./packages/maps/src/offline/native.ts","./packages/maps/src/offline/network.ts","./packages/maps/src/offline/noop.ts","./packages/maps/src/offline/offline-options.ts","./packages/maps/src/offline/opfs-file-store.ts","./packages/maps/src/offline/pack-selection.ts","./packages/maps/src/offline/platform.ts","./packages/maps/src/offline/platform.web.ts","./packages/maps/src/offline/registration.ts","./packages/maps/src/offline/registry.ts","./packages/maps/src/offline/sha256.ts","./packages/maps/src/offline/size-guard.ts","./packages/maps/src/offline/style-composer.ts","./packages/maps/src/offline/transport.ts","./packages/maps/src/offline/types.ts","./packages/maps/src/offline/use-offline-map-options.ts","./packages/maps/src/offline/__tests__/department-data.test.ts","./packages/maps/src/offline/__tests__/downloader.test.ts","./packages/maps/src/offline/__tests__/expo-background-transfer.test.ts","./packages/maps/src/offline/__tests__/fake-expo-file-system.ts","./packages/maps/src/offline/__tests__/glyphs.test.ts","./packages/maps/src/offline/__tests__/helpers.ts","./packages/maps/src/offline/__tests__/manager-sync.test.ts","./packages/maps/src/offline/__tests__/manager.test.ts","./packages/maps/src/offline/__tests__/manifest-client.test.ts","./packages/maps/src/offline/__tests__/manifest.test.ts","./packages/maps/src/offline/__tests__/mode-switch.test.ts","./packages/maps/src/offline/__tests__/native-adapters.test.ts","./packages/maps/src/offline/__tests__/offline-options.test.tsx","./packages/maps/src/offline/__tests__/opfs-file-store.test.ts","./packages/maps/src/offline/__tests__/pack-selection.test.ts","./packages/maps/src/offline/__tests__/registry.test.ts","./packages/maps/src/offline/__tests__/sha256.test.ts","./packages/maps/src/offline/__tests__/style-composer.test.ts","./packages/maps/src/pmtiles/register-pmtiles.ts","./packages/maps/src/pmtiles/register-pmtiles.web.ts","./packages/maps/src/provider/context.ts","./packages/maps/src/provider/map-center.ts","./packages/maps/src/provider/map-provider.tsx","./packages/maps/src/provider/mapbox-compat.ts","./packages/maps/src/provider/overlay-types.ts","./packages/maps/src/provider/overlays.tsx","./packages/maps/src/provider/routing-traffic.ts","./packages/maps/src/provider/services.ts","./packages/maps/src/provider/style-loader.ts","./packages/maps/src/provider/style-resolution.ts","./packages/maps/src/provider/theme.ts","./packages/maps/src/provider/url-safety.ts","./packages/maps/src/provider/url-signer.ts","./packages/maps/src/rg/camera-utils.ts","./packages/maps/src/rg/index.ts","./packages/maps/src/rg/map-placeholder.tsx","./packages/maps/src/rg/maplibre-css.web.ts","./packages/maps/src/rg/maplibre-loader.web.ts","./packages/maps/src/rg/offline-map-badge.tsx","./packages/maps/src/rg/render-context.ts","./packages/maps/src/rg/renderer-setup.ts","./packages/maps/src/rg/renderer-setup.web.ts","./packages/maps/src/rg/rg-map.native.tsx","./packages/maps/src/rg/rg-map.tsx","./packages/maps/src/rg/rg-map.web.tsx","./packages/maps/src/rg/route-layer.tsx","./packages/maps/src/rg/style-props.ts","./packages/maps/src/rg/traffic-layer.tsx","./packages/maps/src/rg/types.ts","./packages/maps/src/rg/use-view-style.ts","./packages/models/src/index.ts","./packages/models/src/__tests__/models.test.ts","./packages/models/src/v4/basev4request.ts","./packages/models/src/v4/index.ts","./packages/models/src/v4/auth/authstate.ts","./packages/models/src/v4/auth/authtokens.ts","./packages/models/src/v4/auth/index.ts","./packages/models/src/v4/auth/login.ts","./packages/models/src/v4/auth/profile.ts","./packages/models/src/v4/autofills/autofillsresult.ts","./packages/models/src/v4/autofills/autofillsresultdata.ts","./packages/models/src/v4/autofills/index.ts","./packages/models/src/v4/awaitingresources/awaitingresourcesresultdata.ts","./packages/models/src/v4/awaitingresources/index.ts","./packages/models/src/v4/awaitingresources/swaggertypes.ts","./packages/models/src/v4/calendar/calendaritemresult.ts","./packages/models/src/v4/calendar/calendaritemresultdata.ts","./packages/models/src/v4/calendar/calendaritemtyperesultdata.ts","./packages/models/src/v4/calendar/calendaritemtypesresult.ts","./packages/models/src/v4/calendar/calendaritemsresult.ts","./packages/models/src/v4/calendar/index.ts","./packages/models/src/v4/calendar/setcalendarattendingresult.ts","./packages/models/src/v4/callfiles/callfileresultdata.ts","./packages/models/src/v4/callfiles/callfilesresult.ts","./packages/models/src/v4/callfiles/index.ts","./packages/models/src/v4/callfiles/savecallfileresult.ts","./packages/models/src/v4/callfiles/swaggertypes.ts","./packages/models/src/v4/callnotes/callnoteresultdata.ts","./packages/models/src/v4/callnotes/callnotesresult.ts","./packages/models/src/v4/callnotes/index.ts","./packages/models/src/v4/callnotes/savecallnoteresult.ts","./packages/models/src/v4/callnotes/swaggertypes.ts","./packages/models/src/v4/callpriorities/callprioritiesresult.ts","./packages/models/src/v4/callpriorities/callpriorityresultdata.ts","./packages/models/src/v4/callpriorities/index.ts","./packages/models/src/v4/callprotocols/callprotocolsresult.ts","./packages/models/src/v4/callprotocols/callprotocolsresultdata.ts","./packages/models/src/v4/callprotocols/index.ts","./packages/models/src/v4/calltypes/calltyperesultdata.ts","./packages/models/src/v4/calltypes/calltypesresult.ts","./packages/models/src/v4/calltypes/index.ts","./packages/models/src/v4/callvideofeeds/callvideofeedenums.ts","./packages/models/src/v4/callvideofeeds/callvideofeedresult.ts","./packages/models/src/v4/callvideofeeds/callvideofeedresultdata.ts","./packages/models/src/v4/callvideofeeds/callvideofeedsresult.ts","./packages/models/src/v4/callvideofeeds/deletecallvideofeedresult.ts","./packages/models/src/v4/callvideofeeds/index.ts","./packages/models/src/v4/callvideofeeds/savecallvideofeedresult.ts","./packages/models/src/v4/callvideofeeds/swaggertypes.ts","./packages/models/src/v4/calls/activecallsresult.ts","./packages/models/src/v4/calls/archivedcallsresult.ts","./packages/models/src/v4/calls/callextradataresult.ts","./packages/models/src/v4/calls/callextradataresultdata.ts","./packages/models/src/v4/calls/callresult.ts","./packages/models/src/v4/calls/callresultdata.ts","./packages/models/src/v4/calls/callsiteinforesult.ts","./packages/models/src/v4/calls/callyearsresult.ts","./packages/models/src/v4/calls/closecallresult.ts","./packages/models/src/v4/calls/deletecallresult.ts","./packages/models/src/v4/calls/dispatchcallnowresult.ts","./packages/models/src/v4/calls/dispatchedeventresultdata.ts","./packages/models/src/v4/calls/index.ts","./packages/models/src/v4/calls/locationhistoryresult.ts","./packages/models/src/v4/calls/newcallfieldpolicyresultdata.ts","./packages/models/src/v4/calls/pendingcallsresult.ts","./packages/models/src/v4/calls/savecallresult.ts","./packages/models/src/v4/calls/scheduledcallsresult.ts","./packages/models/src/v4/calls/statusdestinationsources.ts","./packages/models/src/v4/calls/swaggertypes.ts","./packages/models/src/v4/calls/updatecallresult.ts","./packages/models/src/v4/calls/updatedispatchtimeresult.ts","./packages/models/src/v4/certifications/certifications.ts","./packages/models/src/v4/certifications/index.ts","./packages/models/src/v4/certifications/swaggertypes.ts","./packages/models/src/v4/chat/chatenums.ts","./packages/models/src/v4/chat/chatevents.ts","./packages/models/src/v4/chat/chatinputs.ts","./packages/models/src/v4/chat/chatmodels.ts","./packages/models/src/v4/chat/chatbotmodels.ts","./packages/models/src/v4/chat/index.ts","./packages/models/src/v4/chat/outbox.ts","./packages/models/src/v4/chat/swaggertypes.ts","./packages/models/src/v4/checkin/callpersonnelcheckinstatusresult.ts","./packages/models/src/v4/checkin/callpersonnelcheckinstatusresultdata.ts","./packages/models/src/v4/checkin/checkinenums.ts","./packages/models/src/v4/checkin/checkinrecordresult.ts","./packages/models/src/v4/checkin/checkinrecordresultdata.ts","./packages/models/src/v4/checkin/checkintimerstatusresult.ts","./packages/models/src/v4/checkin/checkintimerstatusresultdata.ts","./packages/models/src/v4/checkin/index.ts","./packages/models/src/v4/checkin/performcheckinresult.ts","./packages/models/src/v4/checkin/resolvedcheckintimerresult.ts","./packages/models/src/v4/checkin/resolvedcheckintimerresultdata.ts","./packages/models/src/v4/checkintimers/index.ts","./packages/models/src/v4/checkintimers/swaggertypes.ts","./packages/models/src/v4/checklists/checklists.ts","./packages/models/src/v4/checklists/index.ts","./packages/models/src/v4/checklists/swaggertypes.ts","./packages/models/src/v4/common/index.ts","./packages/models/src/v4/common/swaggertypes.ts","./packages/models/src/v4/communicationtests/index.ts","./packages/models/src/v4/communicationtests/recordpushresponseinput.ts","./packages/models/src/v4/communicationtests/recordpushresponseresult.ts","./packages/models/src/v4/configs/getconfigresult.ts","./packages/models/src/v4/configs/getconfigresultdata.ts","./packages/models/src/v4/configs/getsystemconfigresult.ts","./packages/models/src/v4/configs/getsystemconfigresultdata.ts","./packages/models/src/v4/configs/getterminologyresult.ts","./packages/models/src/v4/configs/index.ts","./packages/models/src/v4/contactfiles/contactfilesresult.ts","./packages/models/src/v4/contactfiles/index.ts","./packages/models/src/v4/contacts/contactcategoryresultdata.ts","./packages/models/src/v4/contacts/contactnoteresultdata.ts","./packages/models/src/v4/contacts/contactnotesresult.ts","./packages/models/src/v4/contacts/contactpreplanresult.ts","./packages/models/src/v4/contacts/contactresult.ts","./packages/models/src/v4/contacts/contactresultdata.ts","./packages/models/src/v4/contacts/contactscategoriesresult.ts","./packages/models/src/v4/contacts/contactsresult.ts","./packages/models/src/v4/contacts/index.ts","./packages/models/src/v4/contacts/savecontactinput.ts","./packages/models/src/v4/costrecovery/index.ts","./packages/models/src/v4/costrecovery/swaggertypes.ts","./packages/models/src/v4/counts/badgecountsresult.ts","./packages/models/src/v4/counts/index.ts","./packages/models/src/v4/customstatuses/customstatedetailtypes.ts","./packages/models/src/v4/customstatuses/customstatusresultdata.ts","./packages/models/src/v4/customstatuses/customstatusesresult.ts","./packages/models/src/v4/customstatuses/index.ts","./packages/models/src/v4/dataprotection/index.ts","./packages/models/src/v4/dataprotection/swaggertypes.ts","./packages/models/src/v4/deployments/index.ts","./packages/models/src/v4/deployments/swaggertypes.ts","./packages/models/src/v4/destinations/destinationentitytypes.ts","./packages/models/src/v4/destinations/index.ts","./packages/models/src/v4/device/index.ts","./packages/models/src/v4/device/nativepushunregistrationinput.ts","./packages/models/src/v4/device/pushregistrationinput.ts","./packages/models/src/v4/device/pushregistrationresult.ts","./packages/models/src/v4/device/pushregistrationunitinput.ts","./packages/models/src/v4/device/webpushunregistrationinput.ts","./packages/models/src/v4/dispatch/getcalltemplatesresult.ts","./packages/models/src/v4/dispatch/getcalltemplatesresultdata.ts","./packages/models/src/v4/dispatch/getgroupsforcallgridresult.ts","./packages/models/src/v4/dispatch/getgroupsforcallgridresultdata.ts","./packages/models/src/v4/dispatch/getpersonnelforcallgridresult.ts","./packages/models/src/v4/dispatch/getpersonnelforcallgridresultdata.ts","./packages/models/src/v4/dispatch/getrolesforcallgridresult.ts","./packages/models/src/v4/dispatch/getrolesforcallgridresultdata.ts","./packages/models/src/v4/dispatch/getsetunitstateresult.ts","./packages/models/src/v4/dispatch/getsetunitstateresultdata.ts","./packages/models/src/v4/dispatch/index.ts","./packages/models/src/v4/dispatch/nearestunitboard.ts","./packages/models/src/v4/dispatch/newcallformresult.ts","./packages/models/src/v4/dispatch/newcallformresultdata.ts","./packages/models/src/v4/featuretoggles/index.ts","./packages/models/src/v4/featuretoggles/swaggertypes.ts","./packages/models/src/v4/forms/formdataautomationresult.ts","./packages/models/src/v4/forms/formresult.ts","./packages/models/src/v4/forms/formresultdata.ts","./packages/models/src/v4/forms/formsresult.ts","./packages/models/src/v4/forms/index.ts","./packages/models/src/v4/geocoding/index.ts","./packages/models/src/v4/geocoding/swaggertypes.ts","./packages/models/src/v4/groups/groupresult.ts","./packages/models/src/v4/groups/groupsresult.ts","./packages/models/src/v4/groups/groupsresultdata.ts","./packages/models/src/v4/groups/index.ts","./packages/models/src/v4/health/healthresult.ts","./packages/models/src/v4/health/healthresultdata.ts","./packages/models/src/v4/health/index.ts","./packages/models/src/v4/inbox/index.ts","./packages/models/src/v4/inbox/swaggertypes.ts","./packages/models/src/v4/incidentcommand/commanddefinition.ts","./packages/models/src/v4/incidentcommand/incidentcommandenums.ts","./packages/models/src/v4/incidentcommand/incidentcommandmodels.ts","./packages/models/src/v4/incidentcommand/incidentreport.ts","./packages/models/src/v4/incidentcommand/index.ts","./packages/models/src/v4/incidentcommand/resourceincidentview.ts","./packages/models/src/v4/incidentcommand/resourceincidentviewresult.ts","./packages/models/src/v4/inventory/index.ts","./packages/models/src/v4/inventory/inventory.ts","./packages/models/src/v4/mapping/activelayerresultdata.ts","./packages/models/src/v4/mapping/custommapresultdata.ts","./packages/models/src/v4/mapping/getallactivelayersresult.ts","./packages/models/src/v4/mapping/getmapdataandmarkersdata.ts","./packages/models/src/v4/mapping/getmapdataandmarkersresult.ts","./packages/models/src/v4/mapping/getmapiconlegendresult.ts","./packages/models/src/v4/mapping/getmapiconlegendresultdata.ts","./packages/models/src/v4/mapping/getmaplayersresult.ts","./packages/models/src/v4/mapping/getmaplayersresultdata.ts","./packages/models/src/v4/mapping/index.ts","./packages/models/src/v4/mapping/indoormapresultdata.ts","./packages/models/src/v4/mapping/mappingresults.ts","./packages/models/src/v4/mapping/poilayerdata.ts","./packages/models/src/v4/mapping/poiresult.ts","./packages/models/src/v4/mapping/poiresultdata.ts","./packages/models/src/v4/mapping/poityperesultdata.ts","./packages/models/src/v4/mapping/poitypesresult.ts","./packages/models/src/v4/mapping/poisresult.ts","./packages/models/src/v4/messages/deletemessageresult.ts","./packages/models/src/v4/messages/getrecipientsresult.ts","./packages/models/src/v4/messages/index.ts","./packages/models/src/v4/messages/messagerecipientinput.ts","./packages/models/src/v4/messages/messageresult.ts","./packages/models/src/v4/messages/messageresultdata.ts","./packages/models/src/v4/messages/messagesresult.ts","./packages/models/src/v4/messages/recipientsresultdata.ts","./packages/models/src/v4/messages/respondtomessageresult.ts","./packages/models/src/v4/messages/sendmessageresult.ts","./packages/models/src/v4/messages/swaggertypes.ts","./packages/models/src/v4/notes/index.ts","./packages/models/src/v4/notes/notecategoryresult.ts","./packages/models/src/v4/notes/notecategoryresultdata.ts","./packages/models/src/v4/notes/noteresult.ts","./packages/models/src/v4/notes/noteresultdata.ts","./packages/models/src/v4/notes/notesresult.ts","./packages/models/src/v4/notes/savenoteinput.ts","./packages/models/src/v4/notes/savenoteresult.ts","./packages/models/src/v4/notes/swaggertypes.ts","./packages/models/src/v4/operations/index.ts","./packages/models/src/v4/operations/operations.ts","./packages/models/src/v4/passkeys/index.ts","./packages/models/src/v4/passkeys/swaggertypes.ts","./packages/models/src/v4/personnel/filterresultdata.ts","./packages/models/src/v4/personnel/getallpersonnelinfosresult.ts","./packages/models/src/v4/personnel/getpersonnelfilteroptionsresult.ts","./packages/models/src/v4/personnel/index.ts","./packages/models/src/v4/personnel/personnelinforesult.ts","./packages/models/src/v4/personnel/personnelinforesultdata.ts","./packages/models/src/v4/personnellocation/index.ts","./packages/models/src/v4/personnellocation/personnellocationresult.ts","./packages/models/src/v4/personnellocation/personnellocationresultdata.ts","./packages/models/src/v4/personnellocation/savepersonnellocationinput.ts","./packages/models/src/v4/personnellocation/savepersonnellocationresult.ts","./packages/models/src/v4/personnellocation/swaggertypes.ts","./packages/models/src/v4/personnelstaffing/getcurrentstaffingresult.ts","./packages/models/src/v4/personnelstaffing/getcurrentstaffingresultdata.ts","./packages/models/src/v4/personnelstaffing/index.ts","./packages/models/src/v4/personnelstaffing/savepersonstaffinginput.ts","./packages/models/src/v4/personnelstaffing/savepersonstaffingresult.ts","./packages/models/src/v4/personnelstaffing/savepersonsstaffingsinput.ts","./packages/models/src/v4/personnelstaffing/savepersonsstaffingsresult.ts","./packages/models/src/v4/personnelstaffing/swaggertypes.ts","./packages/models/src/v4/personnelstatuses/getcurrentstatusresult.ts","./packages/models/src/v4/personnelstatuses/getcurrentstatusresultdata.ts","./packages/models/src/v4/personnelstatuses/index.ts","./packages/models/src/v4/personnelstatuses/savepersonstatusinput.ts","./packages/models/src/v4/personnelstatuses/savepersonstatusresult.ts","./packages/models/src/v4/personnelstatuses/savepersonsstatusesinput.ts","./packages/models/src/v4/personnelstatuses/savepersonsstatusesresult.ts","./packages/models/src/v4/records/deployments.ts","./packages/models/src/v4/records/index.ts","./packages/models/src/v4/records/records.ts","./packages/models/src/v4/records/swaggertypes.ts","./packages/models/src/v4/roles/index.ts","./packages/models/src/v4/roles/roleresultdata.ts","./packages/models/src/v4/roles/rolesresult.ts","./packages/models/src/v4/routes/directionsresultdata.ts","./packages/models/src/v4/routes/index.ts","./packages/models/src/v4/routes/routedeviationresultdata.ts","./packages/models/src/v4/routes/routeinputs.ts","./packages/models/src/v4/routes/routeinstanceresultdata.ts","./packages/models/src/v4/routes/routeinstancestopresultdata.ts","./packages/models/src/v4/routes/routeplanresultdata.ts","./packages/models/src/v4/routes/routeresults.ts","./packages/models/src/v4/routing/index.ts","./packages/models/src/v4/routing/routingdata.ts","./packages/models/src/v4/routing/routingresults.ts","./packages/models/src/v4/runcards/dispatchrecommendationresultdata.ts","./packages/models/src/v4/runcards/index.ts","./packages/models/src/v4/runcards/runcardresultdata.ts","./packages/models/src/v4/runcards/swaggertypes.ts","./packages/models/src/v4/security/departmentrightsresult.ts","./packages/models/src/v4/security/departmentrightsresultdata.ts","./packages/models/src/v4/security/index.ts","./packages/models/src/v4/shifts/index.ts","./packages/models/src/v4/shifts/ondutypersonresultdata.ts","./packages/models/src/v4/shifts/ondutypersonnelresult.ts","./packages/models/src/v4/shifts/pendingapprovalsresult.ts","./packages/models/src/v4/shifts/pendingshiftsignupresultdata.ts","./packages/models/src/v4/shifts/shiftactionresult.ts","./packages/models/src/v4/shifts/shiftdayresult.ts","./packages/models/src/v4/shifts/shiftdayresultdata.ts","./packages/models/src/v4/shifts/shiftdaysresult.ts","./packages/models/src/v4/shifts/shiftenums.ts","./packages/models/src/v4/shifts/shiftpersonoptionresultdata.ts","./packages/models/src/v4/shifts/shiftpersonoptionsresult.ts","./packages/models/src/v4/shifts/shiftresult.ts","./packages/models/src/v4/shifts/shiftresultdata.ts","./packages/models/src/v4/shifts/shifttraderesult.ts","./packages/models/src/v4/shifts/shifttraderesultdata.ts","./packages/models/src/v4/shifts/shifttradesresult.ts","./packages/models/src/v4/shifts/shiftsresult.ts","./packages/models/src/v4/shifts/signupshiftdayresult.ts","./packages/models/src/v4/shifts/swaggertypes.ts","./packages/models/src/v4/sso/index.ts","./packages/models/src/v4/sso/swaggertypes.ts","./packages/models/src/v4/statuses/index.ts","./packages/models/src/v4/statuses/statusesresult.ts","./packages/models/src/v4/statuses/statusesresultdata.ts","./packages/models/src/v4/statuses/unittypestatusresultdata.ts","./packages/models/src/v4/statuses/unittypestatusesresult.ts","./packages/models/src/v4/templates/callnotetemplateresultdata.ts","./packages/models/src/v4/templates/callnotetemplatesresult.ts","./packages/models/src/v4/templates/callquicktemplateresultdata.ts","./packages/models/src/v4/templates/callquicktemplatesresult.ts","./packages/models/src/v4/templates/index.ts","./packages/models/src/v4/unitlocation/index.ts","./packages/models/src/v4/unitlocation/saveunitlocationinput.ts","./packages/models/src/v4/unitlocation/saveunitlocationresult.ts","./packages/models/src/v4/unitlocation/swaggertypes.ts","./packages/models/src/v4/unitlocation/unitlocationresult.ts","./packages/models/src/v4/unitlocation/unitlocationresultdata.ts","./packages/models/src/v4/unitroles/activeunitroleresultdata.ts","./packages/models/src/v4/unitroles/activeunitrolesresult.ts","./packages/models/src/v4/unitroles/index.ts","./packages/models/src/v4/unitroles/setroleassignmentsforunitresult.ts","./packages/models/src/v4/unitroles/setunitrolesinput.ts","./packages/models/src/v4/unitroles/unitroleresultdata.ts","./packages/models/src/v4/unitroles/unitrolesforunitresult.ts","./packages/models/src/v4/unitstatus/index.ts","./packages/models/src/v4/unitstatus/saveunitstatusinput.ts","./packages/models/src/v4/unitstatus/saveunitstatusresult.ts","./packages/models/src/v4/unitstatus/swaggertypes.ts","./packages/models/src/v4/unitstatus/unitstatusresult.ts","./packages/models/src/v4/unitstatus/unitstatusresultdata.ts","./packages/models/src/v4/unitstatus/unitstatusesresult.ts","./packages/models/src/v4/unitstatusalerts/getunitstatusalertacknowledgementsresult.ts","./packages/models/src/v4/unitstatusalerts/index.ts","./packages/models/src/v4/unitstatusalerts/saveunitstatusalertacknowledgementresult.ts","./packages/models/src/v4/unitstatusalerts/swaggertypes.ts","./packages/models/src/v4/unitstatusalerts/unitstatusalertacknowledgementresultdata.ts","./packages/models/src/v4/units/getunitfilteroptionsresult.ts","./packages/models/src/v4/units/index.ts","./packages/models/src/v4/units/unitinforesult.ts","./packages/models/src/v4/units/unitinforesultdata.ts","./packages/models/src/v4/units/unitresultdata.ts","./packages/models/src/v4/units/unitroledata.ts","./packages/models/src/v4/units/unitsresult.ts","./packages/models/src/v4/userdefinedfields/index.ts","./packages/models/src/v4/userdefinedfields/swaggertypes.ts","./packages/models/src/v4/userdefinedfields/udfdefinitionresult.ts","./packages/models/src/v4/userdefinedfields/udfdefinitionresultdata.ts","./packages/models/src/v4/userdefinedfields/udffieldresultdata.ts","./packages/models/src/v4/userdefinedfields/udffieldvalueinput.ts","./packages/models/src/v4/userdefinedfields/udffieldvalueresultdata.ts","./packages/models/src/v4/userdefinedfields/udffieldvaluesresult.ts","./packages/models/src/v4/voice/canconnecttovoicesessiondata.ts","./packages/models/src/v4/voice/canconnecttovoicesessionresult.ts","./packages/models/src/v4/voice/departmentaudioresult.ts","./packages/models/src/v4/voice/departmentaudioresultstreamdata.ts","./packages/models/src/v4/voice/departmentvoiceresult.ts","./packages/models/src/v4/voice/departmentvoiceresultdata.ts","./packages/models/src/v4/voice/index.ts","./packages/models/src/v4/voice/voicesessionconnectionresult.ts","./packages/models/src/v4/voice/voicesessionconnectionresultdata.ts","./packages/models/src/v4/weatheralerts/activeweatheralertsresult.ts","./packages/models/src/v4/weatheralerts/index.ts","./packages/models/src/v4/weatheralerts/swaggertypes.ts","./packages/models/src/v4/weatheralerts/weatheralertenums.ts","./packages/models/src/v4/weatheralerts/weatheralertresult.ts","./packages/models/src/v4/weatheralerts/weatheralertresultdata.ts","./packages/models/src/v4/weatheralerts/weatheralertsettingsdata.ts","./packages/models/src/v4/weatheralerts/weatheralertsettingsresult.ts","./packages/models/src/v4/weatheralerts/weatheralertsourceresultdata.ts","./packages/models/src/v4/weatheralerts/weatheralertsourcesresult.ts","./packages/models/src/v4/weatheralerts/weatheralertzoneresultdata.ts","./packages/models/src/v4/weatheralerts/weatheralertzonesresult.ts","./packages/models/src/v4/workorders/index.ts","./packages/models/src/v4/workorders/swaggertypes.ts","./packages/models/src/v4/workorders/workorders.ts","./packages/models/src/v4/workforce/index.ts","./packages/models/src/v4/workforce/swaggertypes.ts","./packages/navigation/src/configure.ts","./packages/navigation/src/index.ts","./packages/navigation/src/__tests__/ducking-voice.test.ts","./packages/navigation/src/__tests__/engine.test.ts","./packages/navigation/src/__tests__/fixtures.ts","./packages/navigation/src/__tests__/incidents.test.ts","./packages/navigation/src/__tests__/navigation-store.test.ts","./packages/navigation/src/__tests__/progress-reports.test.ts","./packages/navigation/src/components/incident-chips.tsx","./packages/navigation/src/components/maneuver-banner.tsx","./packages/navigation/src/components/maneuver-icon.tsx","./packages/navigation/src/components/navigation-map.tsx","./packages/navigation/src/components/navigation-session-view.tsx","./packages/navigation/src/components/trip-bar.tsx","./packages/navigation/src/components/use-distance-text.ts","./packages/navigation/src/components/use-navigation-fixes.ts","./packages/navigation/src/engine/format.ts","./packages/navigation/src/engine/geo.ts","./packages/navigation/src/engine/guidance.ts","./packages/navigation/src/engine/incidents.ts","./packages/navigation/src/engine/index.ts","./packages/navigation/src/engine/maneuver.ts","./packages/navigation/src/engine/polyline.ts","./packages/navigation/src/engine/progress.ts","./packages/navigation/src/engine/route-model.ts","./packages/navigation/src/engine/simulation.ts","./packages/navigation/src/store/ducking-voice.ts","./packages/navigation/src/store/navigation-store.ts","./packages/navigation/src/store/progress-reports.ts","./packages/navigation/src/store/route-sources.ts","./packages/navigation/src/store/services.ts","./packages/notifications/src/badge.ts","./packages/notifications/src/channels.ts","./packages/notifications/src/configure.ts","./packages/notifications/src/deep-links.ts","./packages/notifications/src/desktop-notifications.ts","./packages/notifications/src/event-codes.ts","./packages/notifications/src/index.ts","./packages/notifications/src/local-notification-data.ts","./packages/notifications/src/local-notifications.ts","./packages/notifications/src/local-notifications.web.ts","./packages/notifications/src/notification-sound.ts","./packages/notifications/src/novu-provider.tsx","./packages/notifications/src/novu.ts","./packages/notifications/src/push-modal-store.ts","./packages/notifications/src/push-notification.ts","./packages/notifications/src/push-notification.web.ts","./packages/notifications/src/push-types.ts","./packages/notifications/src/registration-storage.ts","./packages/notifications/src/sounds.ts","./packages/notifications/src/__tests__/badge.test.ts","./packages/notifications/src/__tests__/channels.test.ts","./packages/notifications/src/__tests__/deep-links.test.ts","./packages/notifications/src/__tests__/desktop-notifications.test.ts","./packages/notifications/src/__tests__/event-codes.test.ts","./packages/notifications/src/__tests__/helpers.ts","./packages/notifications/src/__tests__/local-notifications.test.ts","./packages/notifications/src/__tests__/notification-sound.test.ts","./packages/notifications/src/__tests__/novu.test.ts","./packages/notifications/src/__tests__/push-modal-store.test.ts","./packages/notifications/src/__tests__/push-notification.test.ts","./packages/notifications/src/__tests__/push-notification.web.test.ts","./packages/notifications/src/__tests__/push-registration.test.ts","./packages/notifications/src/__tests__/sound-files.test.ts","./packages/notifications/src/__tests__/mocks/core.ts","./packages/notifications/src/__tests__/mocks/expo-audio.ts","./packages/notifications/src/__tests__/mocks/expo-device.ts","./packages/notifications/src/__tests__/mocks/expo-notifications.ts","./packages/notifications/src/__tests__/mocks/notifee.ts","./packages/offline/src/index.ts","./packages/offline/src/runtime.ts","./packages/offline/src/testing.ts","./packages/offline/src/__tests__/runtime.test.ts","./packages/offline/src/__tests__/fakes/expo-crypto.ts","./packages/offline/src/__tests__/fakes/expo-secure-store.ts","./packages/offline/src/__tests__/fakes/expo-sqlite.ts","./packages/offline/src/__tests__/fakes/mmkv.ts","./packages/offline/src/__tests__/fakes/netinfo.ts","./packages/offline/src/__tests__/helpers/index.ts","./packages/offline/src/api-cache/durable-api-cache.ts","./packages/offline/src/api-cache/__tests__/durable-api-cache.test.ts","./packages/offline/src/db/indexeddb-persistence.ts","./packages/offline/src/db/memory-db.ts","./packages/offline/src/db/open-db.ts","./packages/offline/src/db/open-db.web.ts","./packages/offline/src/db/schema.ts","./packages/offline/src/db/sql.ts","./packages/offline/src/db/sqlite-db.ts","./packages/offline/src/db/types.ts","./packages/offline/src/db/__tests__/durable-db.test.ts","./packages/offline/src/db/__tests__/indexeddb-persistence.test.ts","./packages/offline/src/db/__tests__/schema.test.ts","./packages/offline/src/kv/contract-keys.ts","./packages/offline/src/kv/durable-kv.ts","./packages/offline/src/kv/keys.ts","./packages/offline/src/kv/memory-backend.ts","./packages/offline/src/kv/mmkv-backend.ts","./packages/offline/src/kv/open-storage.ts","./packages/offline/src/kv/platform.ts","./packages/offline/src/kv/platform.web.ts","./packages/offline/src/kv/secure-store.ts","./packages/offline/src/kv/types.ts","./packages/offline/src/kv/web-backend.ts","./packages/offline/src/kv/zustand-storage.ts","./packages/offline/src/kv/__tests__/durable-kv.test.ts","./packages/offline/src/kv/__tests__/open-storage.test.ts","./packages/offline/src/kv/__tests__/web-backend.test.ts","./packages/offline/src/maintenance/areas.ts","./packages/offline/src/maintenance/retention.ts","./packages/offline/src/maintenance/run-maintenance.ts","./packages/offline/src/maintenance/scheduler.ts","./packages/offline/src/maintenance/storage-manager.ts","./packages/offline/src/maintenance/__tests__/maintenance.test.ts","./packages/offline/src/maintenance/__tests__/storage-manager.test.ts","./packages/offline/src/outbox/cipher.ts","./packages/offline/src/outbox/classify.ts","./packages/offline/src/outbox/connectivity.ts","./packages/offline/src/outbox/outbox.ts","./packages/offline/src/outbox/registry.ts","./packages/offline/src/outbox/schema.ts","./packages/offline/src/outbox/types.ts","./packages/offline/src/outbox/__tests__/cipher-classify.test.ts","./packages/offline/src/outbox/__tests__/connectivity.test.ts","./packages/offline/src/outbox/__tests__/outbox.test.ts","./packages/offline/src/reset/registry.ts","./packages/offline/src/reset/wipe.ts","./packages/offline/src/reset/__tests__/wipe.test.ts","./packages/offline/src/sync/orchestrator.ts","./packages/offline/src/sync/__tests__/orchestrator.test.ts","./packages/offline/src/util/base64.ts","./packages/offline/src/util/ids.ts","./packages/offline/src/util/logger.ts","./packages/offline/src/util/mutex.ts","./packages/offline/src/util/scope.ts","./packages/realtime/src/constants.ts","./packages/realtime/src/event-bus.ts","./packages/realtime/src/events.ts","./packages/realtime/src/index.ts","./packages/realtime/src/lifecycle.ts","./packages/realtime/src/live-locations.ts","./packages/realtime/src/realtime-config.ts","./packages/realtime/src/realtime-store.ts","./packages/realtime/src/refetch.ts","./packages/realtime/src/signalr-service.ts","./packages/realtime/src/watchdog.ts","./packages/realtime/src/__tests__/hooks.test.tsx","./packages/realtime/src/__tests__/lifecycle.test.ts","./packages/realtime/src/__tests__/live-locations.test.ts","./packages/realtime/src/__tests__/realtime-store.test.ts","./packages/realtime/src/__tests__/refetch.test.ts","./packages/realtime/src/__tests__/signalr-service.test.ts","./packages/realtime/src/__tests__/watchdog.test.ts","./packages/realtime/src/__tests__/fakes/expo-constants.ts","./packages/realtime/src/__tests__/fakes/react-native-logs.ts","./packages/realtime/src/__tests__/fakes/react-native.ts","./packages/realtime/src/__tests__/fakes/signalr.ts","./packages/realtime/src/__tests__/helpers/realtime-test-env.ts","./packages/realtime/src/hooks/use-live-locations.ts","./packages/realtime/src/hooks/use-realtime-event.ts","./packages/realtime/src/hooks/use-realtime-lifecycle.ts","./packages/realtime/src/hooks/use-realtime-refetch.ts","./packages/realtime/src/hooks/use-realtime-status.ts","./packages/ui/src/bottom-sheet.tsx","./packages/ui/src/count-badge.tsx","./packages/ui/src/cover.tsx","./packages/ui/src/edge-scroll.tsx","./packages/ui/src/focus-aware-status-bar.tsx","./packages/ui/src/index.ts","./packages/ui/src/list.tsx","./packages/ui/src/lucide-icons.tsx","./packages/ui/src/progress-bar.tsx","./packages/ui/src/scroll-row.tsx","./packages/ui/src/shared-tabs.tsx","./packages/ui/src/side-drawer.tsx","./packages/ui/src/tab-bar.tsx","./packages/ui/src/text-field-metrics.ts","./packages/ui/src/use-route-tab.ts","./packages/ui/src/use-settled-flag.ts","./packages/ui/src/wheel-scroll.ts","./packages/ui/src/wheel-scroll.web.ts","./packages/ui/src/__tests__/actionsheet.test.tsx","./packages/ui/src/__tests__/count-badge.test.tsx","./packages/ui/src/__tests__/image-viewer.test.tsx","./packages/ui/src/__tests__/overlays.test.tsx","./packages/ui/src/__tests__/primitives.test.tsx","./packages/ui/src/__tests__/screen-header.test.tsx","./packages/ui/src/__tests__/scroll-row.test.tsx","./packages/ui/src/__tests__/shared-tabs-reveal.test.tsx","./packages/ui/src/__tests__/tab-bar.test.tsx","./packages/ui/src/__tests__/use-route-tab.test.tsx","./packages/ui/src/__tests__/use-settled-flag.test.ts","./packages/ui/src/__tests__/wheel-scroll.test.ts","./packages/ui/src/accordion/index.tsx","./packages/ui/src/actionsheet/index.tsx","./packages/ui/src/alert/index.tsx","./packages/ui/src/alert-dialog/index.tsx","./packages/ui/src/avatar/index.tsx","./packages/ui/src/badge/index.tsx","./packages/ui/src/box/index.tsx","./packages/ui/src/box/index.web.tsx","./packages/ui/src/box/styles.tsx","./packages/ui/src/button/index.tsx","./packages/ui/src/card/index.tsx","./packages/ui/src/card/index.web.tsx","./packages/ui/src/card/styles.tsx","./packages/ui/src/center/index.tsx","./packages/ui/src/center/index.web.tsx","./packages/ui/src/center/styles.tsx","./packages/ui/src/checkbox/index.tsx","./packages/ui/src/common/date-time-field.tsx","./packages/ui/src/common/date-time-picker.ts","./packages/ui/src/common/header-back-button.tsx","./packages/ui/src/common/index.ts","./packages/ui/src/common/loading.tsx","./packages/ui/src/common/realtime-status-banner.tsx","./packages/ui/src/common/screen-header.tsx","./packages/ui/src/common/sync-attention-toast.tsx","./packages/ui/src/common/type-icon.tsx","./packages/ui/src/common/zero-state.tsx","./packages/ui/src/common/__tests__/common.test.tsx","./packages/ui/src/common/__tests__/date-time-field.test.tsx","./packages/ui/src/divider/index.tsx","./packages/ui/src/drawer/index.tsx","./packages/ui/src/fab/index.tsx","./packages/ui/src/flat-list/index.tsx","./packages/ui/src/flat-list/__tests__/flat-list.test.tsx","./packages/ui/src/form-control/index.tsx","./packages/ui/src/gluestack-image/index.tsx","./packages/ui/src/gluestack-ui-provider/index.tsx","./packages/ui/src/gluestack-ui-provider/index.web.tsx","./packages/ui/src/gluestack-ui-provider/script.ts","./packages/ui/src/gluestack-ui-provider/__tests__/web-provider.test.tsx","./packages/ui/src/grid/index.tsx","./packages/ui/src/grid/index.web.tsx","./packages/ui/src/grid/styles.tsx","./packages/ui/src/heading/index.tsx","./packages/ui/src/heading/index.web.tsx","./packages/ui/src/heading/styles.tsx","./packages/ui/src/hstack/index.tsx","./packages/ui/src/hstack/index.web.tsx","./packages/ui/src/hstack/styles.tsx","./packages/ui/src/html-renderer/document.ts","./packages/ui/src/html-renderer/html-entities.ts","./packages/ui/src/html-renderer/html-sanitizer.ts","./packages/ui/src/html-renderer/html.ts","./packages/ui/src/html-renderer/index.tsx","./packages/ui/src/html-renderer/index.web.tsx","./packages/ui/src/html-renderer/__tests__/html-renderer.test.tsx","./packages/ui/src/html-renderer/__tests__/html-sanitizer.test.ts","./packages/ui/src/html-renderer/__tests__/rich-text.test.ts","./packages/ui/src/icon/index.tsx","./packages/ui/src/icon/index.web.tsx","./packages/ui/src/image/index.tsx","./packages/ui/src/image-background/index.tsx","./packages/ui/src/image-viewer/index.tsx","./packages/ui/src/image-viewer/zoom.ts","./packages/ui/src/input/index.tsx","./packages/ui/src/input-accessory-view/index.tsx","./packages/ui/src/keyboard-avoiding-view/bottom-anchored.tsx","./packages/ui/src/keyboard-avoiding-view/index.tsx","./packages/ui/src/keyboard-avoiding-view/keep-focused-input-visible.ts","./packages/ui/src/keyboard-avoiding-view/keyboard-aware-scroll-view.tsx","./packages/ui/src/keyboard-avoiding-view/keyboard-aware-scroll-view.web.tsx","./packages/ui/src/keyboard-avoiding-view/__tests__/bottom-anchored.test.tsx","./packages/ui/src/keyboard-avoiding-view/__tests__/keyboard-avoidance-rules.test.ts","./packages/ui/src/keyboard-avoiding-view/__tests__/keyboard-avoidance.test.tsx","./packages/ui/src/layout/index.ts","./packages/ui/src/layout/platform.ts","./packages/ui/src/layout/responsive.tsx","./packages/ui/src/layout/use-form-factor.ts","./packages/ui/src/layout/__tests__/use-form-factor.test.tsx","./packages/ui/src/link/index.tsx","./packages/ui/src/menu/index.tsx","./packages/ui/src/modal/index.tsx","./packages/ui/src/native-modal/index.tsx","./packages/ui/src/popover/index.tsx","./packages/ui/src/portal/index.tsx","./packages/ui/src/pressable/index.tsx","./packages/ui/src/pressable/__tests__/nested-pressables.test.ts","./packages/ui/src/progress/index.tsx","./packages/ui/src/radio/index.tsx","./packages/ui/src/refresh-control/index.tsx","./packages/ui/src/safe-area-view/index.tsx","./packages/ui/src/scroll-view/index.tsx","./packages/ui/src/scroll-view/__tests__/scroll-view.test.tsx","./packages/ui/src/section-list/index.tsx","./packages/ui/src/select/index.tsx","./packages/ui/src/select/select-actionsheet.tsx","./packages/ui/src/skeleton/index.tsx","./packages/ui/src/skeleton/index.web.tsx","./packages/ui/src/skeleton/styles.tsx","./packages/ui/src/slider/index.tsx","./packages/ui/src/spinner/index.tsx","./packages/ui/src/status-bar/index.tsx","./packages/ui/src/switch/index.tsx","./packages/ui/src/text/index.tsx","./packages/ui/src/text/index.web.tsx","./packages/ui/src/text/line-height.ts","./packages/ui/src/text/styles.tsx","./packages/ui/src/text/__tests__/text-line-height.test.tsx","./packages/ui/src/text/__tests__/text-web.test.tsx","./packages/ui/src/textarea/index.tsx","./packages/ui/src/theme/color-scheme.tsx","./packages/ui/src/theme/index.ts","./packages/ui/src/theme/readable-color.ts","./packages/ui/src/theme/tokens.ts","./packages/ui/src/theme/use-theme-colors.ts","./packages/ui/src/theme/__tests__/readable-color.test.tsx","./packages/ui/src/theme/__tests__/theme.test.tsx","./packages/ui/src/toast/index.ts","./packages/ui/src/toast/primitive.tsx","./packages/ui/src/toast/store.ts","./packages/ui/src/toast/toast-container.tsx","./packages/ui/src/toast/toast-message.tsx","./packages/ui/src/toast/use-toast.ts","./packages/ui/src/toast/__tests__/store.test.ts","./packages/ui/src/toast/__tests__/toast-container.test.tsx","./packages/ui/src/tooltip/index.tsx","./packages/ui/src/utils/dom-props.ts","./packages/ui/src/utils/errors.ts","./packages/ui/src/utils/i18n.ts","./packages/ui/src/utils/index.ts","./packages/ui/src/utils/__tests__/dom-props.test.ts","./packages/ui/src/utils/__tests__/errors.test.ts","./packages/ui/src/view/index.tsx","./packages/ui/src/virtualized-list/index.tsx","./packages/ui/src/vstack/index.tsx","./packages/ui/src/vstack/index.web.tsx","./packages/ui/src/vstack/styles.tsx","./packages/voice/src/capabilities.ts","./packages/voice/src/index.ts","./packages/voice/src/initialize.ts","./packages/voice/src/runtime.ts","./packages/voice/src/__tests__/voice-runtime.test.ts","./packages/voice/src/__tests__/web-platform.test.ts","./packages/voice/src/__tests__/fakes/expo-audio.ts","./packages/voice/src/__tests__/fakes/expo-device.ts","./packages/voice/src/__tests__/fakes/livekit-client.ts","./packages/voice/src/__tests__/fakes/livekit-react-native-webrtc.ts","./packages/voice/src/__tests__/fakes/livekit-react-native.ts","./packages/voice/src/__tests__/fakes/notifee.ts","./packages/voice/src/__tests__/fakes/react-native-ble-manager.ts","./packages/voice/src/__tests__/fakes/react-native-callkeep.ts","./packages/voice/src/__tests__/fakes/react-native.ts","./packages/voice/src/__tests__/fakes/resgrid-core.ts","./packages/voice/src/__tests__/helpers/connectivity.ts","./packages/voice/src/__tests__/helpers/test-doubles.ts","./packages/voice/src/__tests__/helpers/variant.ts","./packages/voice/src/audio/audio-route.ts","./packages/voice/src/audio/audio-session.ts","./packages/voice/src/audio/audio-session.web.ts","./packages/voice/src/audio/clip-player.ts","./packages/voice/src/audio/voice-sounds.ts","./packages/voice/src/audio/voice-sounds.web.ts","./packages/voice/src/audio/__tests__/audio-session.test.ts","./packages/voice/src/audio-streams/audio-stream-store.ts","./packages/voice/src/audio-streams/stream-player.ts","./packages/voice/src/audio-streams/stream-player.web.ts","./packages/voice/src/audio-streams/stream-source.ts","./packages/voice/src/audio-streams/types.ts","./packages/voice/src/audio-streams/__tests__/audio-stream-store.test.ts","./packages/voice/src/audio-streams/__tests__/stream-player.web.test.ts","./packages/voice/src/bluetooth/bluetooth-audio-store.ts","./packages/voice/src/bluetooth/bluetooth-service-base.ts","./packages/voice/src/bluetooth/bluetooth-service-native.ts","./packages/voice/src/bluetooth/bluetooth-service-web.ts","./packages/voice/src/bluetooth/bluetooth-service.ts","./packages/voice/src/bluetooth/bluetooth-service.web.ts","./packages/voice/src/bluetooth/device-profiles.ts","./packages/voice/src/bluetooth/types.ts","./packages/voice/src/bluetooth/__tests__/bluetooth-service-native.test.ts","./packages/voice/src/bluetooth/__tests__/bluetooth-service-web.test.ts","./packages/voice/src/bluetooth/__tests__/device-profiles.test.ts","./packages/voice/src/callkeep/callkeep-native.ts","./packages/voice/src/callkeep/callkeep-service.ts","./packages/voice/src/callkeep/callkeep-service.web.ts","./packages/voice/src/callkeep/mute-filter.ts","./packages/voice/src/callkeep/types.ts","./packages/voice/src/callkeep/__tests__/callkeep.test.ts","./packages/voice/src/desktop/desktop-ptt-controller.ts","./packages/voice/src/desktop/__tests__/desktop-ptt-controller.test.ts","./packages/voice/src/headset/headset-button-service.ts","./packages/voice/src/headset/headset-button-service.web.ts","./packages/voice/src/headset/headset-ptt-preference.ts","./packages/voice/src/headset/types.ts","./packages/voice/src/headset/__tests__/headset-button-service.test.ts","./packages/voice/src/hooks/use-audio-streams.ts","./packages/voice/src/hooks/use-desktop-ptt.ts","./packages/voice/src/hooks/use-ptt.ts","./packages/voice/src/hooks/use-voice-channels.ts","./packages/voice/src/hooks/use-voice-status.ts","./packages/voice/src/hooks/__tests__/use-desktop-ptt.test.tsx","./packages/voice/src/hooks/__tests__/voice-hooks.test.tsx","./packages/voice/src/livekit/foreground-service.ts","./packages/voice/src/livekit/foreground-service.web.ts","./packages/voice/src/livekit/livekit-client.ts","./packages/voice/src/livekit/livekit-store.ts","./packages/voice/src/livekit/microphone-permission.ts","./packages/voice/src/livekit/microphone-permission.web.ts","./packages/voice/src/livekit/remote-audio.ts","./packages/voice/src/livekit/remote-audio.web.ts","./packages/voice/src/livekit/types.ts","./packages/voice/src/livekit/__tests__/livekit-store.test.ts","./packages/voice/src/ptt/microphone-controller.ts","./packages/voice/src/ptt/ptt-press-controller.ts","./packages/voice/src/ptt/__tests__/ptt-press-controller.test.ts","./packages/voice/src/util/async.ts","./packages/voice/src/util/bytes.ts","./packages/voice/src/util/network.ts","./apps/omni/app-updates.native.ts","./apps/omni/app-updates.ts","./apps/omni/app.config.ts","./apps/omni/background-location-entry.ts","./apps/omni/background-location.ts","./apps/omni/call-file-reader.native.ts","./apps/omni/call-file-reader.ts","./apps/omni/detail-nav.ts","./apps/omni/expo-types.d.ts","./apps/omni/index.ts","./apps/omni/map-config-refresh.ts","./apps/omni/map-icons.ts","./apps/omni/map-worker.ts","./apps/omni/map-worker.web.ts","./apps/omni/nativewind-env.d.ts","./apps/omni/navigation-wiring.ts","./apps/omni/notification-sounds.native.ts","./apps/omni/notification-sounds.ts","./apps/omni/offline-maps-platform.native.ts","./apps/omni/offline-maps-platform.ts","./apps/omni/offline-maps-wiring.ts","./apps/omni/react-native-css-env.d.ts","./apps/omni/records-context.ts","./apps/omni/theme-styles.ts","./apps/omni/theme-styles.web.ts","./apps/omni/voice-sounds.ts","./apps/omni/__tests__/app-config.test.ts","./apps/omni/__tests__/app-updates.test.ts","./apps/omni/__tests__/background-location.test.ts","./apps/omni/__tests__/dev-api-proxy.test.ts","./apps/omni/__tests__/lucide-icon-imports.test.ts","./apps/omni/__tests__/map-config-refresh.test.ts","./apps/omni/__tests__/maplibre-worker.test.ts","./apps/omni/__tests__/offline-maps-wiring.test.ts","./apps/omni/__tests__/qa-native-flows.test.ts","./apps/omni/__tests__/records-context.test.ts","./apps/omni/__tests__/service-worker.test.ts","./apps/omni/__tests__/sound-assets.test.ts","./apps/omni/__tests__/variant-uniformity.test.ts","./apps/omni/e2e/playwright.config.ts","./apps/omni/e2e/qa-regression.ts","./apps/omni/e2e/desktop/boot.spec.ts","./apps/omni/e2e/pages/app-page.ts","./apps/omni/e2e/pages/home-page.ts","./apps/omni/e2e/pages/login-page.ts","./apps/omni/e2e/qa/define-regression.ts","./apps/omni/e2e/qa/regression.spec.ts","./apps/omni/e2e/qa-desktop/regression.spec.ts","./apps/omni/e2e/support/desktop-fixtures.ts","./apps/omni/e2e/support/electron-app.ts","./apps/omni/e2e/support/qa-departments.ts","./apps/omni/e2e/support/qa-desktop-fixtures.ts","./apps/omni/e2e/support/qa-expected-refusals.ts","./apps/omni/e2e/support/qa-fixtures.ts","./apps/omni/e2e/support/qa-native-flows.ts","./apps/omni/e2e/support/static-server.ts","./apps/omni/e2e/support/variant-fixtures.ts","./apps/omni/e2e/support/variants.ts","./apps/omni/e2e/support/web-fixtures.ts","./apps/omni/e2e/web/boot.spec.ts","./apps/omni/e2e/web/routes-and-maps.spec.ts","./apps/omni/e2e/web/sign-in.spec.ts","./apps/omni/e2e/web/web-push.spec.ts","./apps/omni/stubs/countly-config.web.ts","./apps/omni/stubs/countly-sdk.web.ts","./apps/omni/stubs/livekit-react-native-webrtc.web.ts","./apps/omni/stubs/livekit-react-native.web.ts","./apps/omni/stubs/react-native-ble-manager.web.ts","./apps/omni/variants/bigboard/nav.ts","./apps/omni/variants/bigboard/variant.ts","./apps/omni/variants/dispatch/nav.ts","./apps/omni/variants/dispatch/variant.ts","./apps/omni/variants/ic/nav.ts","./apps/omni/variants/ic/variant.ts","./apps/omni/variants/responder/nav.ts","./apps/omni/variants/responder/variant.ts","./apps/omni/variants/unit/nav.ts","./apps/omni/variants/unit/variant.ts","./apps/omni/feature-providers.tsx","./apps/omni/route-map.tsx","./apps/omni/shell-registrations.tsx","./apps/omni/screens/dashboard.tsx","./apps/omni/screens/dispatch-console.tsx","./apps/omni/screens/map.tsx","./apps/omni/screens/notifications-inbox.tsx","./apps/omni/screens/offline-maps.tsx","./apps/omni/variants/bigboard/app/_layout.tsx","./apps/omni/variants/bigboard/app/maintenance.tsx","./apps/omni/variants/bigboard/app/sso-return.tsx","./apps/omni/variants/bigboard/app/(app)/_layout.tsx","./apps/omni/variants/bigboard/app/(app)/calls.tsx","./apps/omni/variants/bigboard/app/(app)/configure.tsx","./apps/omni/variants/bigboard/app/(app)/contacts.tsx","./apps/omni/variants/bigboard/app/(app)/home.tsx","./apps/omni/variants/bigboard/app/(app)/index.tsx","./apps/omni/variants/bigboard/app/(app)/map.tsx","./apps/omni/variants/bigboard/app/(app)/notes.tsx","./apps/omni/variants/bigboard/app/(app)/personnel.tsx","./apps/omni/variants/bigboard/app/(app)/protocols.tsx","./apps/omni/variants/bigboard/app/(app)/settings.tsx","./apps/omni/variants/bigboard/app/(app)/units.tsx","./apps/omni/variants/bigboard/app/(app)/weather-alerts.tsx","./apps/omni/variants/bigboard/app/(app)/call/[id].tsx","./apps/omni/variants/bigboard/app/(app)/contacts/[id].tsx","./apps/omni/variants/bigboard/app/(app)/notes/[id].tsx","./apps/omni/variants/bigboard/app/(app)/personnel/[id].tsx","./apps/omni/variants/bigboard/app/(app)/protocols/[id].tsx","./apps/omni/variants/bigboard/app/(app)/settings/account-security.tsx","./apps/omni/variants/bigboard/app/(app)/settings/storage-sync.tsx","./apps/omni/variants/bigboard/app/(app)/units/[id].tsx","./apps/omni/variants/bigboard/app/(app)/weather-alerts/[id].tsx","./apps/omni/variants/bigboard/app/login/index.tsx","./apps/omni/variants/bigboard/app/login/recovery.tsx","./apps/omni/variants/bigboard/app/login/sso.tsx","./apps/omni/variants/dispatch/app/_layout.tsx","./apps/omni/variants/dispatch/app/lockscreen.tsx","./apps/omni/variants/dispatch/app/maintenance.tsx","./apps/omni/variants/dispatch/app/onboarding.tsx","./apps/omni/variants/dispatch/app/sso-return.tsx","./apps/omni/variants/dispatch/app/(app)/_layout.tsx","./apps/omni/variants/dispatch/app/(app)/archived-calls.tsx","./apps/omni/variants/dispatch/app/(app)/awaiting-resources.tsx","./apps/omni/variants/dispatch/app/(app)/calls.tsx","./apps/omni/variants/dispatch/app/(app)/chat.tsx","./apps/omni/variants/dispatch/app/(app)/chatbot.tsx","./apps/omni/variants/dispatch/app/(app)/contacts.tsx","./apps/omni/variants/dispatch/app/(app)/index.tsx","./apps/omni/variants/dispatch/app/(app)/map.tsx","./apps/omni/variants/dispatch/app/(app)/notes.tsx","./apps/omni/variants/dispatch/app/(app)/notifications.tsx","./apps/omni/variants/dispatch/app/(app)/pending-calls.tsx","./apps/omni/variants/dispatch/app/(app)/personnel.tsx","./apps/omni/variants/dispatch/app/(app)/pois.tsx","./apps/omni/variants/dispatch/app/(app)/protocols.tsx","./apps/omni/variants/dispatch/app/(app)/scheduled-calls.tsx","./apps/omni/variants/dispatch/app/(app)/settings.tsx","./apps/omni/variants/dispatch/app/(app)/units.tsx","./apps/omni/variants/dispatch/app/(app)/weather-alerts.tsx","./apps/omni/variants/dispatch/app/(app)/call/[id].tsx","./apps/omni/variants/dispatch/app/(app)/call/new.tsx","./apps/omni/variants/dispatch/app/(app)/call/[id]/command.tsx","./apps/omni/variants/dispatch/app/(app)/call/[id]/edit.tsx","./apps/omni/variants/dispatch/app/(app)/chat/[channelid].tsx","./apps/omni/variants/dispatch/app/(app)/contacts/[id].tsx","./apps/omni/variants/dispatch/app/(app)/maps/_layout.tsx","./apps/omni/variants/dispatch/app/(app)/maps/index.tsx","./apps/omni/variants/dispatch/app/(app)/maps/search.tsx","./apps/omni/variants/dispatch/app/(app)/maps/custom/[id].tsx","./apps/omni/variants/dispatch/app/(app)/maps/indoor/[id].tsx","./apps/omni/variants/dispatch/app/(app)/notes/[id].tsx","./apps/omni/variants/dispatch/app/(app)/operations/[id].tsx","./apps/omni/variants/dispatch/app/(app)/operations/_layout.tsx","./apps/omni/variants/dispatch/app/(app)/operations/index.tsx","./apps/omni/variants/dispatch/app/(app)/personnel/[id].tsx","./apps/omni/variants/dispatch/app/(app)/poi/[id].tsx","./apps/omni/variants/dispatch/app/(app)/protocols/[id].tsx","./apps/omni/variants/dispatch/app/(app)/records/[id].tsx","./apps/omni/variants/dispatch/app/(app)/records/_layout.tsx","./apps/omni/variants/dispatch/app/(app)/records/index.tsx","./apps/omni/variants/dispatch/app/(app)/records/new.tsx","./apps/omni/variants/dispatch/app/(app)/records/connectors/[id].tsx","./apps/omni/variants/dispatch/app/(app)/records/connectors/index.tsx","./apps/omni/variants/dispatch/app/(app)/records/deployments/[id].tsx","./apps/omni/variants/dispatch/app/(app)/records/deployments/index.tsx","./apps/omni/variants/dispatch/app/(app)/settings/account-security.tsx","./apps/omni/variants/dispatch/app/(app)/settings/offline-maps.tsx","./apps/omni/variants/dispatch/app/(app)/settings/storage-sync.tsx","./apps/omni/variants/dispatch/app/(app)/units/[id].tsx","./apps/omni/variants/dispatch/app/(app)/weather-alerts/[id].tsx","./apps/omni/variants/dispatch/app/(app)/weather-alerts/settings.tsx","./apps/omni/variants/dispatch/app/login/index.tsx","./apps/omni/variants/dispatch/app/login/recovery.tsx","./apps/omni/variants/dispatch/app/login/shared-device.tsx","./apps/omni/variants/dispatch/app/login/sso.tsx","./apps/omni/variants/ic/app/_layout.tsx","./apps/omni/variants/ic/app/maintenance.tsx","./apps/omni/variants/ic/app/onboarding.tsx","./apps/omni/variants/ic/app/sso-return.tsx","./apps/omni/variants/ic/app/(app)/_layout.tsx","./apps/omni/variants/ic/app/(app)/calls.tsx","./apps/omni/variants/ic/app/(app)/chat.tsx","./apps/omni/variants/ic/app/(app)/chatbot.tsx","./apps/omni/variants/ic/app/(app)/command.tsx","./apps/omni/variants/ic/app/(app)/contacts.tsx","./apps/omni/variants/ic/app/(app)/incidents.tsx","./apps/omni/variants/ic/app/(app)/index.tsx","./apps/omni/variants/ic/app/(app)/map.tsx","./apps/omni/variants/ic/app/(app)/notifications.tsx","./apps/omni/variants/ic/app/(app)/resource-map.tsx","./apps/omni/variants/ic/app/(app)/settings.tsx","./apps/omni/variants/ic/app/(app)/weather-alerts.tsx","./apps/omni/variants/ic/app/(app)/call/[id].tsx","./apps/omni/variants/ic/app/(app)/call/new.tsx","./apps/omni/variants/ic/app/(app)/call/[id]/edit.tsx","./apps/omni/variants/ic/app/(app)/chat/[channelid].tsx","./apps/omni/variants/ic/app/(app)/command-log/[callid].tsx","./apps/omni/variants/ic/app/(app)/command-map/[callid].tsx","./apps/omni/variants/ic/app/(app)/contacts/[id].tsx","./apps/omni/variants/ic/app/(app)/maps/_layout.tsx","./apps/omni/variants/ic/app/(app)/maps/index.tsx","./apps/omni/variants/ic/app/(app)/maps/search.tsx","./apps/omni/variants/ic/app/(app)/maps/custom/[id].tsx","./apps/omni/variants/ic/app/(app)/maps/indoor/[id].tsx","./apps/omni/variants/ic/app/(app)/operations/[id].tsx","./apps/omni/variants/ic/app/(app)/operations/_layout.tsx","./apps/omni/variants/ic/app/(app)/operations/index.tsx","./apps/omni/variants/ic/app/(app)/poi/[id].tsx","./apps/omni/variants/ic/app/(app)/records/[id].tsx","./apps/omni/variants/ic/app/(app)/records/_layout.tsx","./apps/omni/variants/ic/app/(app)/records/index.tsx","./apps/omni/variants/ic/app/(app)/records/new.tsx","./apps/omni/variants/ic/app/(app)/records/connectors/[id].tsx","./apps/omni/variants/ic/app/(app)/records/connectors/index.tsx","./apps/omni/variants/ic/app/(app)/records/deployments/[id].tsx","./apps/omni/variants/ic/app/(app)/records/deployments/index.tsx","./apps/omni/variants/ic/app/(app)/settings/account-security.tsx","./apps/omni/variants/ic/app/(app)/settings/offline-maps.tsx","./apps/omni/variants/ic/app/(app)/settings/storage-sync.tsx","./apps/omni/variants/ic/app/(app)/weather-alerts/[id].tsx","./apps/omni/variants/ic/app/login/index.tsx","./apps/omni/variants/ic/app/login/recovery.tsx","./apps/omni/variants/ic/app/login/shared-device.tsx","./apps/omni/variants/ic/app/login/sso.tsx","./apps/omni/variants/responder/app/_layout.tsx","./apps/omni/variants/responder/app/maintenance.tsx","./apps/omni/variants/responder/app/onboarding.tsx","./apps/omni/variants/responder/app/sso-return.tsx","./apps/omni/variants/responder/app/(app)/_layout.tsx","./apps/omni/variants/responder/app/(app)/approve-sign-in.tsx","./apps/omni/variants/responder/app/(app)/calendar.tsx","./apps/omni/variants/responder/app/(app)/chat.tsx","./apps/omni/variants/responder/app/(app)/chatbot.tsx","./apps/omni/variants/responder/app/(app)/checklists.tsx","./apps/omni/variants/responder/app/(app)/contacts.tsx","./apps/omni/variants/responder/app/(app)/index.tsx","./apps/omni/variants/responder/app/(app)/map.tsx","./apps/omni/variants/responder/app/(app)/notes.tsx","./apps/omni/variants/responder/app/(app)/notifications.tsx","./apps/omni/variants/responder/app/(app)/protocols.tsx","./apps/omni/variants/responder/app/(app)/settings.tsx","./apps/omni/variants/responder/app/(app)/weather-alerts.tsx","./apps/omni/variants/responder/app/(app)/call/[id].tsx","./apps/omni/variants/responder/app/(app)/call/new.tsx","./apps/omni/variants/responder/app/(app)/call/[id]/edit.tsx","./apps/omni/variants/responder/app/(app)/certifications/[id].tsx","./apps/omni/variants/responder/app/(app)/certifications/edit.tsx","./apps/omni/variants/responder/app/(app)/certifications/index.tsx","./apps/omni/variants/responder/app/(app)/chat/[channelid].tsx","./apps/omni/variants/responder/app/(app)/contacts/[id].tsx","./apps/omni/variants/responder/app/(app)/home/_layout.tsx","./apps/omni/variants/responder/app/(app)/home/calls.tsx","./apps/omni/variants/responder/app/(app)/home/index.tsx","./apps/omni/variants/responder/app/(app)/home/personnel.tsx","./apps/omni/variants/responder/app/(app)/home/units.tsx","./apps/omni/variants/responder/app/(app)/messages/[id].tsx","./apps/omni/variants/responder/app/(app)/messages/index.tsx","./apps/omni/variants/responder/app/(app)/messages/new.tsx","./apps/omni/variants/responder/app/(app)/notes/[id].tsx","./apps/omni/variants/responder/app/(app)/operations/[id].tsx","./apps/omni/variants/responder/app/(app)/operations/_layout.tsx","./apps/omni/variants/responder/app/(app)/operations/index.tsx","./apps/omni/variants/responder/app/(app)/personnel/[id].tsx","./apps/omni/variants/responder/app/(app)/poi/[id].tsx","./apps/omni/variants/responder/app/(app)/protocols/[id].tsx","./apps/omni/variants/responder/app/(app)/records/[id].tsx","./apps/omni/variants/responder/app/(app)/records/_layout.tsx","./apps/omni/variants/responder/app/(app)/records/index.tsx","./apps/omni/variants/responder/app/(app)/records/new.tsx","./apps/omni/variants/responder/app/(app)/records/connectors/[id].tsx","./apps/omni/variants/responder/app/(app)/records/connectors/index.tsx","./apps/omni/variants/responder/app/(app)/records/deployments/[id].tsx","./apps/omni/variants/responder/app/(app)/records/deployments/index.tsx","./apps/omni/variants/responder/app/(app)/settings/account-security.tsx","./apps/omni/variants/responder/app/(app)/settings/offline-maps.tsx","./apps/omni/variants/responder/app/(app)/settings/storage-sync.tsx","./apps/omni/variants/responder/app/(app)/shifts/index.tsx","./apps/omni/variants/responder/app/(app)/shifts/day/[id].tsx","./apps/omni/variants/responder/app/(app)/shifts/trade/[id].tsx","./apps/omni/variants/responder/app/(app)/units/[id].tsx","./apps/omni/variants/responder/app/(app)/weather-alerts/[id].tsx","./apps/omni/variants/responder/app/(app)/work-orders/[id].tsx","./apps/omni/variants/responder/app/(app)/work-orders/index.tsx","./apps/omni/variants/responder/app/(app)/work-orders/new.tsx","./apps/omni/variants/responder/app/login/index.tsx","./apps/omni/variants/responder/app/login/recovery.tsx","./apps/omni/variants/responder/app/login/sso.tsx","./apps/omni/variants/unit/app/_layout.tsx","./apps/omni/variants/unit/app/maintenance.tsx","./apps/omni/variants/unit/app/onboarding.tsx","./apps/omni/variants/unit/app/sso-return.tsx","./apps/omni/variants/unit/app/(app)/_layout.tsx","./apps/omni/variants/unit/app/(app)/calls.tsx","./apps/omni/variants/unit/app/(app)/chat.tsx","./apps/omni/variants/unit/app/(app)/chatbot.tsx","./apps/omni/variants/unit/app/(app)/checklists.tsx","./apps/omni/variants/unit/app/(app)/console.tsx","./apps/omni/variants/unit/app/(app)/contacts.tsx","./apps/omni/variants/unit/app/(app)/index.tsx","./apps/omni/variants/unit/app/(app)/map.tsx","./apps/omni/variants/unit/app/(app)/navigate.tsx","./apps/omni/variants/unit/app/(app)/notes.tsx","./apps/omni/variants/unit/app/(app)/notifications.tsx","./apps/omni/variants/unit/app/(app)/protocols.tsx","./apps/omni/variants/unit/app/(app)/settings.tsx","./apps/omni/variants/unit/app/(app)/weather-alerts.tsx","./apps/omni/variants/unit/app/(app)/call/[id].tsx","./apps/omni/variants/unit/app/(app)/call/new.tsx","./apps/omni/variants/unit/app/(app)/call/[id]/edit.tsx","./apps/omni/variants/unit/app/(app)/chat/[channelid].tsx","./apps/omni/variants/unit/app/(app)/contacts/[id].tsx","./apps/omni/variants/unit/app/(app)/inventory/_layout.tsx","./apps/omni/variants/unit/app/(app)/inventory/index.tsx","./apps/omni/variants/unit/app/(app)/inventory/count/[id].tsx","./apps/omni/variants/unit/app/(app)/maps/_layout.tsx","./apps/omni/variants/unit/app/(app)/maps/index.tsx","./apps/omni/variants/unit/app/(app)/maps/search.tsx","./apps/omni/variants/unit/app/(app)/maps/custom/[id].tsx","./apps/omni/variants/unit/app/(app)/maps/indoor/[id].tsx","./apps/omni/variants/unit/app/(app)/notes/[id].tsx","./apps/omni/variants/unit/app/(app)/operations/[id].tsx","./apps/omni/variants/unit/app/(app)/operations/_layout.tsx","./apps/omni/variants/unit/app/(app)/operations/index.tsx","./apps/omni/variants/unit/app/(app)/protocols/[id].tsx","./apps/omni/variants/unit/app/(app)/records/[id].tsx","./apps/omni/variants/unit/app/(app)/records/_layout.tsx","./apps/omni/variants/unit/app/(app)/records/index.tsx","./apps/omni/variants/unit/app/(app)/records/new.tsx","./apps/omni/variants/unit/app/(app)/records/connectors/[id].tsx","./apps/omni/variants/unit/app/(app)/records/connectors/index.tsx","./apps/omni/variants/unit/app/(app)/records/deployments/[id].tsx","./apps/omni/variants/unit/app/(app)/records/deployments/index.tsx","./apps/omni/variants/unit/app/(app)/routes/_layout.tsx","./apps/omni/variants/unit/app/(app)/routes/active.tsx","./apps/omni/variants/unit/app/(app)/routes/directions.tsx","./apps/omni/variants/unit/app/(app)/routes/index.tsx","./apps/omni/variants/unit/app/(app)/routes/start.tsx","./apps/omni/variants/unit/app/(app)/routes/history/[planid].tsx","./apps/omni/variants/unit/app/(app)/routes/history/instance/[id].tsx","./apps/omni/variants/unit/app/(app)/routes/poi/[id].tsx","./apps/omni/variants/unit/app/(app)/routes/stop/[id].tsx","./apps/omni/variants/unit/app/(app)/routes/stop/contact.tsx","./apps/omni/variants/unit/app/(app)/settings/account-security.tsx","./apps/omni/variants/unit/app/(app)/settings/offline-maps.tsx","./apps/omni/variants/unit/app/(app)/settings/storage-sync.tsx","./apps/omni/variants/unit/app/(app)/weather-alerts/[id].tsx","./apps/omni/variants/unit/app/login/index.tsx","./apps/omni/variants/unit/app/login/recovery.tsx","./apps/omni/variants/unit/app/login/shared-device.tsx","./apps/omni/variants/unit/app/login/sso.tsx"],"version":"6.0.3"} \ No newline at end of file From 2b061b339c4d05bbc9e9fb55f52593f2d753a13a Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Sun, 8 Nov 2026 23:02:26 -0800 Subject: [PATCH 2/2] RO-T101 PR #26 Fixes --- .claude/skills/omni-offline-first/SKILL.md | 2 +- .../__tests__/command-store.test.ts | 120 +++++++++++++++--- .../__tests__/command-support-store.test.ts | 19 ++- .../src/incident-command/command-handlers.ts | 85 +++++++++++-- .../src/incident-command/command-store.ts | 59 ++++++++- .../incident-command/command-support-store.ts | 10 +- .../__tests__/command-board-view.test.tsx | 17 +++ .../__tests__/incident-command-lib.test.ts | 11 ++ .../components/command-board-view.tsx | 16 ++- .../src/incident-command/i18n.en.json | 2 + .../src/incident-command/lib/permissions.ts | 9 +- packages/i18n/review/missing-translations.csv | 18 +++ packages/i18n/review/summary.json | 2 +- packages/i18n/src/translations/common/ar.json | 4 +- packages/i18n/src/translations/common/de.json | 4 +- packages/i18n/src/translations/common/el.json | 4 +- packages/i18n/src/translations/common/en.json | 4 +- packages/i18n/src/translations/common/es.json | 4 +- packages/i18n/src/translations/common/fr.json | 4 +- packages/i18n/src/translations/common/it.json | 4 +- packages/i18n/src/translations/common/pl.json | 4 +- packages/i18n/src/translations/common/sv.json | 4 +- packages/i18n/src/translations/common/uk.json | 4 +- 23 files changed, 355 insertions(+), 55 deletions(-) diff --git a/.claude/skills/omni-offline-first/SKILL.md b/.claude/skills/omni-offline-first/SKILL.md index 796f2b82..a982acce 100644 --- a/.claude/skills/omni-offline-first/SKILL.md +++ b/.claude/skills/omni-offline-first/SKILL.md @@ -20,7 +20,7 @@ Every Omni app works offline: it reads its shift working set from durable storag classify: (err) => (err.status && err.status < 500 ? 'permanent' : 'retry'), }); ``` -3. **Idempotency:** the event `id` (client UUID) is the idempotency key sent to the server; creates send a client GUID. Never generate ids that change on retry. +3. **Idempotency:** the event `id` (client UUID) is the idempotency key sent to the server; creates send a client GUID. Never generate ids that change on retry. A row shown optimistically under a placeholder id (IC board rows: `local-`) may be acted on before its create is sent; the write queued then still names the placeholder, so the create's `afterAck` records the server's id and the later write's `send` swaps it in (IC `resolvedLocalIds`). Never let such a write reach the server with the placeholder and count as delivered. 4. **Ordering:** use `dependsOn` for real dependencies (e.g. close call only after close command) and `coalesceKey` for newest-wins streams (location fixes). 5. **Scope:** every queued event and stored record carries server + department + user (+ unit). Never replay one user's or server's events as another; hold them (7 days) and show them. 6. **Wipe:** logout, department switch and server switch wipe the working set and queue for that scope (preserved: device prefs like language, theme, server URL). Register stores with the reset registry. diff --git a/packages/data/src/incident-command/__tests__/command-store.test.ts b/packages/data/src/incident-command/__tests__/command-store.test.ts index 5f450343..58aa1664 100644 --- a/packages/data/src/incident-command/__tests__/command-store.test.ts +++ b/packages/data/src/incident-command/__tests__/command-store.test.ts @@ -1,10 +1,23 @@ -import { assignResource, closeCallWith, closeCommand, establishCommand, getAdHocPersonnel, getAdHocUnits, getCommandBoard, getResourceIncidentView, getSyncBundle, moveResource, saveObjective } from '@resgrid/api'; -import { BaseV4Request, type IncidentCommandBoard, type ResourceAssignment } from '@resgrid/models'; +import { + assignResource, + closeCallWith, + closeCommand, + establishCommand, + getAdHocPersonnel, + getAdHocUnits, + getCommandBoard, + getResourceIncidentView, + getSyncBundle, + moveResource, + saveNode, + saveObjective, +} from '@resgrid/api'; +import { BaseV4Request, type CommandStructureNode, type IncidentCommandBoard, type ResourceAssignment } from '@resgrid/models'; import { defaultResetRegistry } from '@resgrid/offline'; import { createDataTestRuntime, type DataTestRuntime, flush, httpError, notFound, ok, persisted } from '../../__tests__/helpers'; import { setDataStorage } from '../../core/persist'; -import { IC_EVENT, incidentOrderKey, useCommandStore, withPendingLocalRows } from '../command-store'; +import { IC_EVENT, incidentOrderKey, RESOLVED_LOCAL_ID_RETENTION_MS, useCommandStore, withPendingLocalRows } from '../command-store'; import { useResourceViewStore } from '../resource-view-store'; jest.mock('@resgrid/api', () => ({ @@ -18,6 +31,7 @@ jest.mock('@resgrid/api', () => ({ getAdHocUnits: jest.fn(), getAdHocPersonnel: jest.fn(), getSyncBundle: jest.fn(), + saveNode: jest.fn(), saveObjective: jest.fn(), getResourceIncidentView: jest.fn(), })); @@ -39,6 +53,20 @@ const board = (callId: number, extra: Partial = {}): Incid /** A v4 success envelope as the API functions resolve it. */ const envelope = (Data: T): BaseV4Request & { Data: T } => ({ ...new BaseV4Request(), Status: 'success', Data }); +/** Unit u1 in lane n1 on call 7's board. */ +const assignmentRow: ResourceAssignment = { + ResourceAssignmentId: 'a1', + IncidentCommandId: 'ic-7', + DepartmentId: 1, + CallId: 7, + CommandStructureNodeId: 'n1', + ResourceKind: 1, + ResourceId: 'u1', + AssignedByUserId: 'u0', + AssignedOn: '2026-11-08T22:00:00Z', + RequirementsWarning: false, +}; + let runtime: DataTestRuntime; beforeEach(async () => { @@ -125,19 +153,7 @@ describe('incident command board (IC stores/command/*)', () => { }); it('moves a resource to a lane through MoveResource and back to the pool through AssignResource', async () => { - const row: ResourceAssignment = { - ResourceAssignmentId: 'a1', - IncidentCommandId: 'ic-7', - DepartmentId: 1, - CallId: 7, - CommandStructureNodeId: 'n1', - ResourceKind: 1, - ResourceId: 'u1', - AssignedByUserId: 'u0', - AssignedOn: '2026-11-08T22:00:00Z', - RequirementsWarning: false, - }; - + const row = assignmentRow; const withRow = board(7, { Assignments: [row] }); jest.mocked(getCommandBoard).mockResolvedValue(envelope(withRow)); jest.mocked(moveResource).mockResolvedValue(envelope(row)); @@ -161,12 +177,82 @@ describe('incident command board (IC stores/command/*)', () => { expect(assignResource).toHaveBeenCalledTimes(1); }); + it('sends writes queued against rows created offline with the ids the server gave those rows', async () => { + const lane: CommandStructureNode = { CommandStructureNodeId: 'n1', IncidentCommandId: 'ic-7', DepartmentId: 1, CallId: 7, Name: 'Interior', NodeType: 0, SortOrder: 0 }; + const row = assignmentRow; + + useCommandStore.setState({ boards: { '7': { callId: '7', board: board(7), adHocUnits: [], adHocPersonnel: [], isProvisional: false, lastRefreshed: 1 } } }); + runtime.connectivity.setOnline(false); + + // Offline: add a lane, assign a resource into it, nest a lane under it, then move the resource back to the pool. + const addLane = await useCommandStore.getState().dispatchCommandEvent(IC_EVENT.saveNode, { + callId: '7', + resolveCommandId: true, + local: { collection: 'Nodes', idField: 'CommandStructureNodeId' }, + body: { CallId: 7, Name: 'Interior', NodeType: 0, ParentNodeId: null, SortOrder: 0 }, + }); + + const assign = await useCommandStore.getState().dispatchCommandEvent(IC_EVENT.assignResource, { + callId: '7', + resolveCommandId: true, + local: { collection: 'Assignments', idField: 'ResourceAssignmentId' }, + body: { CallId: 7, CommandStructureNodeId: `local-${addLane.id}`, ResourceKind: 1, ResourceId: 'u1' }, + }); + + await useCommandStore.getState().dispatchCommandEvent(IC_EVENT.saveNode, { callId: '7', resolveCommandId: true, body: { CallId: 7, Name: 'Floor 2', NodeType: 0, ParentNodeId: `local-${addLane.id}`, SortOrder: 1 } }); + await useCommandStore.getState().dispatchCommandEvent(IC_EVENT.moveResource, { callId: '7', body: { ResourceAssignmentId: `local-${assign.id}`, TargetNodeId: '' } }); + + jest + .mocked(saveNode) + .mockResolvedValueOnce(envelope(lane)) + .mockResolvedValueOnce(envelope({ ...lane, CommandStructureNodeId: 'n2', ParentNodeId: 'n1' })); + jest.mocked(assignResource).mockResolvedValue(envelope(row)); + jest.mocked(getCommandBoard).mockResolvedValue(envelope(board(7, { Nodes: [lane], Assignments: [row] }))); + runtime.connectivity.setOnline(true); + await runtime.outbox.drain(); + await flush(); + + expect(assignResource).toHaveBeenNthCalledWith(1, expect.anything(), expect.objectContaining({ CommandStructureNodeId: 'n1', ResourceId: 'u1', IncidentCommandId: 'ic-7' })); + expect(saveNode).toHaveBeenNthCalledWith(2, expect.anything(), expect.objectContaining({ Name: 'Floor 2', ParentNodeId: 'n1' })); + // The pool move re-saves the server's row, not a row the server never had. + expect(assignResource).toHaveBeenNthCalledWith(2, expect.anything(), { ...row, CommandStructureNodeId: '' }); + expect(useCommandStore.getState().resolvedLocalIds).toEqual({ + [`local-${addLane.id}`]: expect.objectContaining({ callId: '7', serverId: 'n1' }), + [`local-${assign.id}`]: expect.objectContaining({ callId: '7', serverId: 'a1' }), + }); + expect(useCommandStore.getState().pendingLocalRows).toEqual({}); + expect(await runtime.rows()).toEqual([]); + }); + + it('holds back a write whose row is still waiting for its create, and sends an unknown local id as it is', async () => { + useCommandStore.setState({ boards: { '7': { callId: '7', board: board(7), adHocUnits: [], adHocPersonnel: [], isProvisional: false, lastRefreshed: 1 } } }); + useCommandStore.getState().addLocalRow({ eventId: 'e1', callId: '7', collection: 'Assignments', row: { ResourceAssignmentId: 'local-e1', __localEventId: 'e1' } }); + + const held = await useCommandStore.getState().dispatchCommandEvent(IC_EVENT.moveResource, { callId: '7', body: { ResourceAssignmentId: 'local-e1', TargetNodeId: 'n2' } }); + expect(held.outcome).toBe('queued'); + expect(moveResource).not.toHaveBeenCalled(); + await runtime.outbox.discard(held.id, { confirmed: true }); + + // Its create was refused or discarded: nothing on the server has the id, and the server answers for it. + jest.mocked(moveResource).mockResolvedValue(envelope(assignmentRow)); + const orphan = await useCommandStore.getState().dispatchCommandEvent(IC_EVENT.moveResource, { callId: '7', body: { ResourceAssignmentId: 'local-e9', TargetNodeId: 'n2' } }); + expect(orphan.outcome).toBe('sent'); + expect(moveResource).toHaveBeenCalledWith(expect.anything(), { ResourceAssignmentId: 'local-e9', TargetNodeId: 'n2' }); + }); + + it('keeps the server ids of acknowledged creates for the retention period only', () => { + useCommandStore.getState().resolveLocalId('e1', '7', 'a1'); + runtime.clock.advance(RESOLVED_LOCAL_ID_RETENTION_MS + 1); + useCommandStore.getState().resolveLocalId('e2', '7', 'a2'); + expect(Object.keys(useCommandStore.getState().resolvedLocalIds)).toEqual(['local-e2']); + }); + it('syncs every board from /Sync/Bundle and persists in the frozen command-storage shape', async () => { jest.mocked(getSyncBundle).mockResolvedValue(ok({ ServerTimestampMs: 77, Boards: [board(7)], AdHocUnits: [{ CallId: 7, ReleasedOn: null }], AdHocPersonnel: [] }) as never); await useCommandStore.getState().syncFromServer(); expect(useCommandStore.getState().boards['7'].adHocUnits).toHaveLength(1); expect(useCommandStore.getState().lastSyncTimestampMs).toBe(77); - expect(Object.keys(persisted(runtime.kv, 'command-storage')?.state ?? {})).toEqual(['boards', 'activeCallId', 'lastSyncTimestampMs', 'pendingLocalRows']); + expect(Object.keys(persisted(runtime.kv, 'command-storage')?.state ?? {})).toEqual(['boards', 'activeCallId', 'lastSyncTimestampMs', 'pendingLocalRows', 'resolvedLocalIds']); }); it('rehydrates legacy IC boards (no Needs) and repairs a dangling activeCallId', async () => { diff --git a/packages/data/src/incident-command/__tests__/command-support-store.test.ts b/packages/data/src/incident-command/__tests__/command-support-store.test.ts index 310876c3..e7f6ec73 100644 --- a/packages/data/src/incident-command/__tests__/command-support-store.test.ts +++ b/packages/data/src/incident-command/__tests__/command-support-store.test.ts @@ -1,5 +1,5 @@ import { closeIncidentChannels, createIncidentChannel, getAllCommandDefinitions, getCommandBoard, getIncidentVoiceChannels, getMyIncidentCapabilities, transferCommand } from '@resgrid/api'; -import type { IncidentCommandBoard } from '@resgrid/models'; +import { BaseV4Request, type IncidentCapabilitiesResult, type IncidentCommandBoard } from '@resgrid/models'; import { defaultResetRegistry } from '@resgrid/offline'; import { createDataTestRuntime, failure, ok } from '../../__tests__/helpers'; @@ -19,6 +19,9 @@ jest.mock('@resgrid/api', () => ({ getAdHocPersonnel: jest.fn(async () => ({ Data: [], Status: 'success' })), })); +/** `IncidentRoles/GetMyCapabilities` as Core answers it: a success envelope carrying the bitmask itself (or not). */ +const capabilitiesAnswer = (fields: Pick): IncidentCapabilitiesResult => ({ ...new BaseV4Request(), Status: 'success', ...fields }); + const board = (callId: number): IncidentCommandBoard => ({ Command: { IncidentCommandId: `ic-${callId}`, CallId: callId }, @@ -38,16 +41,26 @@ beforeEach(async () => { }); describe('command support store', () => { - it('reads the effective capability bitmask per incident (0 when the server sends none)', async () => { + it('reads the effective capability bitmask per incident (still unknown when the answer has none)', async () => { // The bitmask is on the envelope, as Core sends it (no Data). jest.mocked(getMyIncidentCapabilities).mockResolvedValueOnce({ Status: 'success', Value: 9, Capabilities: ['ViewBoard', 'AssignResources'] } as never); await useCommandSupportStore.getState().fetchCapabilities('7'); expect(getMyIncidentCapabilities).toHaveBeenCalledWith(expect.anything(), { callId: 7 }); expect(useCommandSupportStore.getState().capabilitiesByCall['7']).toBe(9); - jest.mocked(getMyIncidentCapabilities).mockResolvedValueOnce({ Data: null, Status: 'not_found' } as never); + // A member who may do nothing gets 0, which is known (read-only, or not even readable). + jest.mocked(getMyIncidentCapabilities).mockResolvedValueOnce(capabilitiesAnswer({ Value: 0, Capabilities: [] })); await useCommandSupportStore.getState().fetchCapabilities('8', { bypassCache: true }); expect(useCommandSupportStore.getState().capabilitiesByCall['8']).toBe(0); + + // No bitmask says nothing: unknown, so the IC own-board fallback still applies (and a previous answer is kept). + jest.mocked(getMyIncidentCapabilities).mockResolvedValueOnce({ Data: null, Status: 'not_found' } as never); + await useCommandSupportStore.getState().fetchCapabilities('9', { bypassCache: true }); + expect(useCommandSupportStore.getState().capabilitiesByCall).not.toHaveProperty('9'); + + jest.mocked(getMyIncidentCapabilities).mockResolvedValueOnce(capabilitiesAnswer({})); + await useCommandSupportStore.getState().fetchCapabilities('7', { bypassCache: true }); + expect(useCommandSupportStore.getState().capabilitiesByCall['7']).toBe(9); }); it('keeps a failed capability read unloaded and records the error', async () => { diff --git a/packages/data/src/incident-command/command-handlers.ts b/packages/data/src/incident-command/command-handlers.ts index 41b074f2..e9e11853 100644 --- a/packages/data/src/incident-command/command-handlers.ts +++ b/packages/data/src/incident-command/command-handlers.ts @@ -51,8 +51,8 @@ import type { OutboxHandler } from '@resgrid/offline'; import { assertV4Success, unwrapItem } from '../core/envelope'; import { defineDataHandler } from '../core/outbox'; -import { getDataApiContext } from '../core/runtime'; -import { IC_EVENT, type IcEventPayload, type IcEventType, useCommandStore } from './command-store'; +import { getDataApiContext, getDataLogger } from '../core/runtime'; +import { IC_EVENT, type IcEventPayload, type IcEventType, LOCAL_ID_PREFIX, useCommandStore } from './command-store'; class NoIncidentCommandError extends Error { constructor() { @@ -61,6 +61,42 @@ class NoIncidentCommandError extends Error { } } +class PendingCreateError extends Error { + constructor() { + super('The board item this write refers to has not reached the server yet.'); + this.name = 'PendingCreateError'; + } +} + +/** + * The server id for a board row id. A row created offline carries `local-` until its create is acknowledged, + * and writes queued against it meanwhile (moving a resource assigned offline, filling, editing or nesting under a lane + * added offline) still name that id. Writes for one incident replay in order, so by the time such a write is sent its + * create has been acknowledged and has recorded the server's id. A create still queued holds the write back (retried); + * one that was refused or discarded left nothing on the server, and the id goes as it is for the server to answer. + */ +function toServerId(id: string): string; +function toServerId(id: string | null | undefined): string | null | undefined; +function toServerId(id: string | null | undefined): string | null | undefined { + if (!id?.startsWith(LOCAL_ID_PREFIX)) return id; + + const { pendingLocalRows, resolvedLocalIds } = useCommandStore.getState(); + const resolved = resolvedLocalIds[id]; + + if (resolved) return resolved.serverId; + + if (pendingLocalRows[id.slice(LOCAL_ID_PREFIX.length)]) throw new PendingCreateError(); + + return id; +} + +/** How Core answers a board create: the saved row, its id field holding the id the server gave it. */ +interface CreateAck { + Data?: Partial> | null; +} + +const createdId = (ack: CreateAck | null, idField: string): string | null => ack?.Data?.[idField] || null; + /** The server IncidentCommandId for a call (writes queued before the command existed). Retried until the establish lands. */ const resolveIncidentCommandId = async (ctx: ApiContext, callId: string): Promise => { const known = useCommandStore.getState().boards[callId]?.board?.Command?.IncidentCommandId; @@ -74,7 +110,10 @@ const resolveIncidentCommandId = async (ctx: ApiContext, callId: string): Promis return id; }; -const param = (payload: IcEventPayload, name: string): string => payload.params?.[name] ?? ''; +/** A path/query id, as the server knows it. */ +const param = (payload: IcEventPayload, name: string): string => toServerId(payload.params?.[name] ?? ''); + +const nodeWithServerIds = (node: CommandStructureNode): CommandStructureNode => ({ ...node, CommandStructureNodeId: toServerId(node.CommandStructureNodeId), ParentNodeId: toServerId(node.ParentNodeId) }); /** The board's row for an assignment, else the server's (the board may not have refreshed since the write was queued). */ const findAssignment = async (ctx: ApiContext, callId: string, resourceAssignmentId: string): Promise => { @@ -92,7 +131,9 @@ const findAssignment = async (ctx: ApiContext, callId: string, resourceAssignmen * re-saves the assignment through AssignResource, which upserts by id and keeps a blank node as the pool. An * assignment no longer on the board has nothing to move — delivered, like MoveResource's own not_found. */ -const sendMoveResource = async (ctx: ApiContext, callId: string, input: MoveResourceInput): Promise => { +const sendMoveResource = async (ctx: ApiContext, callId: string, queued: MoveResourceInput): Promise => { + const input: MoveResourceInput = { ResourceAssignmentId: toServerId(queued.ResourceAssignmentId), TargetNodeId: toServerId(queued.TargetNodeId) }; + if (input.TargetNodeId?.trim()) return moveResource(ctx, input); const assignment = await findAssignment(ctx, callId, input.ResourceAssignmentId); @@ -114,17 +155,29 @@ const SENDERS: Record = { [IC_EVENT.releaseAdHocUnit]: (ctx, payload) => releaseAdHocUnit(ctx, { incidentAdHocUnitId: param(payload, 'incidentAdHocUnitId') }), [IC_EVENT.createAdHocPersonnel]: (ctx, _payload, body) => createAdHocPersonnel(ctx, body as unknown as IncidentAdHocPersonnel), [IC_EVENT.releaseAdHocPersonnel]: (ctx, payload) => releaseAdHocPersonnel(ctx, { incidentAdHocPersonnelId: param(payload, 'incidentAdHocPersonnelId') }), - [IC_EVENT.saveNode]: (ctx, _payload, body) => saveNode(ctx, body as unknown as CommandStructureNode), - [IC_EVENT.updateNode]: (ctx, _payload, body) => saveNode(ctx, body as unknown as CommandStructureNode), + [IC_EVENT.saveNode]: (ctx, _payload, body) => saveNode(ctx, nodeWithServerIds(body as unknown as CommandStructureNode)), + [IC_EVENT.updateNode]: (ctx, _payload, body) => saveNode(ctx, nodeWithServerIds(body as unknown as CommandStructureNode)), [IC_EVENT.deleteNode]: (ctx, payload) => deleteNode(ctx, { commandStructureNodeId: param(payload, 'commandStructureNodeId') }), - [IC_EVENT.assignResource]: (ctx, _payload, body) => assignResource(ctx, body as unknown as ResourceAssignment), + [IC_EVENT.assignResource]: (ctx, _payload, body) => { + const assignment = body as unknown as ResourceAssignment; + + return assignResource(ctx, { ...assignment, CommandStructureNodeId: toServerId(assignment.CommandStructureNodeId) }); + }, [IC_EVENT.moveResource]: (ctx, payload, body) => sendMoveResource(ctx, payload.callId, body as unknown as MoveResourceInput), [IC_EVENT.releaseResource]: (ctx, payload) => releaseResource(ctx, { resourceAssignmentId: param(payload, 'resourceAssignmentId') }), [IC_EVENT.saveObjective]: (ctx, _payload, body) => saveObjective(ctx, body as unknown as TacticalObjective), [IC_EVENT.completeObjective]: (ctx, payload, body) => completeObjective(ctx, { tacticalObjectiveId: param(payload, 'tacticalObjectiveId') }, body as unknown as CompleteObjectiveInput), - [IC_EVENT.updateObjectiveProgress]: (ctx, _payload, body) => updateObjectiveProgress(ctx, body as unknown as UpdateObjectiveProgressInput), + [IC_EVENT.updateObjectiveProgress]: (ctx, _payload, body) => { + const input = body as unknown as UpdateObjectiveProgressInput; + + return updateObjectiveProgress(ctx, { ...input, TacticalObjectiveId: toServerId(input.TacticalObjectiveId) }); + }, [IC_EVENT.saveNeed]: (ctx, _payload, body) => saveIncidentNeed(ctx, body as unknown as IncidentNeed), - [IC_EVENT.setNeedStatus]: (ctx, _payload, body) => setNeedStatus(ctx, body as unknown as SetNeedStatusInput), + [IC_EVENT.setNeedStatus]: (ctx, _payload, body) => { + const input = body as unknown as SetNeedStatusInput; + + return setNeedStatus(ctx, { ...input, IncidentNeedId: toServerId(input.IncidentNeedId) }); + }, [IC_EVENT.startTimer]: (ctx, _payload, body) => startIncidentTimer(ctx, body as unknown as IncidentTimer), [IC_EVENT.acknowledgeTimer]: (ctx, payload) => acknowledgeIncidentTimer(ctx, { incidentTimerId: param(payload, 'incidentTimerId') }), [IC_EVENT.saveAnnotation]: (ctx, _payload, body) => saveAnnotation(ctx, body as unknown as IncidentMapAnnotation), @@ -142,7 +195,7 @@ const createIcHandler = (type: IcEventType): OutboxHandler { const ctx = getDataApiContext(); @@ -152,8 +205,18 @@ const createIcHandler = (type: IcEventType): OutboxHandler { + afterAck: async (event, result) => { const store = useCommandStore.getState(); + const { local } = event.payload; + + if (local) { + // SAFETY: `send` returned what a board create answers, the v4 envelope with the saved row in `Data`, whose id fields are strings. + const serverId = createdId(result as CreateAck | null, local.idField); + + if (serverId) store.resolveLocalId(event.id, event.payload.callId, serverId); + else getDataLogger().warn({ message: 'Board create acknowledged without its id; writes queued against its row go as they are', context: { type } }); + } + store.removeLocalRow(event.id); if (type === IC_EVENT.establishCommand) store.markBoardEstablished(event.payload.callId); diff --git a/packages/data/src/incident-command/command-store.ts b/packages/data/src/incident-command/command-store.ts index 4513341a..0ed9bc7a 100644 --- a/packages/data/src/incident-command/command-store.ts +++ b/packages/data/src/incident-command/command-store.ts @@ -75,6 +75,25 @@ export interface PendingLocalRow { row: Record; } +/** Optimistic rows carry `local-` ids until the server acknowledges their create. */ +export const LOCAL_ID_PREFIX = 'local-'; + +/** The server id an acknowledged create gave its optimistic row (keyed by the row's `local-` id). */ +export interface ResolvedLocalId { + callId: string; + serverId: string; + resolvedAt: number; +} + +/** + * Retention for `resolvedLocalIds`: writes queued against an optimistic row replay right after its create (one order + * per incident), so a mapping is only needed while those writes are still being retried. + */ +export const RESOLVED_LOCAL_ID_RETENTION_MS = 7 * 24 * 60 * 60 * 1000; + +const pruneResolvedLocalIds = (resolved: Record, now: number): Record => + Object.fromEntries(Object.entries(resolved).filter(([, entry]) => now - entry.resolvedAt <= RESOLVED_LOCAL_ID_RETENTION_MS)); + export interface EndCommandOptions { closeCall?: { type: number; notes?: string; sendNotification?: boolean }; } @@ -91,6 +110,11 @@ interface CommandData extends SliceMeta { activeCallId: string | null; lastSyncTimestampMs: number; pendingLocalRows: Record; + /** + * Optimistic row ids whose create the server acknowledged → the server's id, so writes queued against the row while it + * was pending (moving a resource assigned offline, filling or editing a lane added offline) reach the real row. + */ + resolvedLocalIds: Record; /** IC incident list (GetCommandList). */ summaries: IncidentCommandSummary[]; } @@ -110,10 +134,12 @@ export interface CommandState extends CommandData { // Used by the outbox handlers. addLocalRow: (row: PendingLocalRow) => void; removeLocalRow: (eventId: string) => void; + /** Records the server id an acknowledged create gave the row `local-` (and drops mappings past retention). */ + resolveLocalId: (eventId: string, callId: string, serverId: string) => void; markBoardEstablished: (callId: string) => void; } -const initialData = (): CommandData => ({ ...emptySliceMeta(), boards: {}, activeCallId: null, lastSyncTimestampMs: 0, pendingLocalRows: {}, summaries: [] }); +const initialData = (): CommandData => ({ ...emptySliceMeta(), boards: {}, activeCallId: null, lastSyncTimestampMs: 0, pendingLocalRows: {}, resolvedLocalIds: {}, summaries: [] }); const emptyBoardState = (callId: string, provisional: boolean): CommandBoardState => ({ callId, board: null, adHocUnits: [], adHocPersonnel: [], isProvisional: provisional, lastRefreshed: null }); @@ -288,6 +314,13 @@ export const useCommandStore = create()( return { pendingLocalRows, ...(entry ? { boards: { ...state.boards, [row.callId]: entry } } : {}) }; }), + resolveLocalId: (eventId, callId, serverId) => + set((state) => { + const now = dataNow(); + + return { resolvedLocalIds: { ...pruneResolvedLocalIds(state.resolvedLocalIds, now), [`${LOCAL_ID_PREFIX}${eventId}`]: { callId, serverId, resolvedAt: now } } }; + }), + markBoardEstablished: (callId) => set((state) => { const entry = state.boards[callId]; @@ -295,19 +328,33 @@ export const useCommandStore = create()( }), }; }, - dataPersistOptions>({ - // Frozen: IC's persisted command store (boards, activeCallId, lastSyncTimestampMs); pendingLocalRows is additive. + dataPersistOptions>({ + // Frozen: IC's persisted command store (boards, activeCallId, lastSyncTimestampMs); pendingLocalRows and + // resolvedLocalIds are additive. name: 'command-storage', - partialize: (state) => ({ boards: state.boards, activeCallId: state.activeCallId, lastSyncTimestampMs: state.lastSyncTimestampMs, pendingLocalRows: state.pendingLocalRows }), + partialize: (state) => ({ + boards: state.boards, + activeCallId: state.activeCallId, + lastSyncTimestampMs: state.lastSyncTimestampMs, + pendingLocalRows: state.pendingLocalRows, + resolvedLocalIds: state.resolvedLocalIds, + }), merge: (persisted, current) => { - const value = (persisted ?? {}) as Partial>; + const value = (persisted ?? {}) as Partial>; const boards = value.boards && typeof value.boards === 'object' ? value.boards : {}; // Older IC builds persisted boards before Needs existed. for (const [callId, entry] of Object.entries(boards)) { if (entry?.board) boards[callId] = { ...entry, board: { ...entry.board, Needs: Array.isArray(entry.board.Needs) ? entry.board.Needs : [] } }; } const activeCallId = value.activeCallId && boards[value.activeCallId] ? value.activeCallId : (Object.keys(boards)[0] ?? null); - return { ...current, boards, activeCallId, lastSyncTimestampMs: value.lastSyncTimestampMs ?? 0, pendingLocalRows: value.pendingLocalRows ?? {} }; + return { + ...current, + boards, + activeCallId, + lastSyncTimestampMs: value.lastSyncTimestampMs ?? 0, + pendingLocalRows: value.pendingLocalRows ?? {}, + resolvedLocalIds: pruneResolvedLocalIds(value.resolvedLocalIds ?? {}, dataNow()), + }; }, }) ) diff --git a/packages/data/src/incident-command/command-support-store.ts b/packages/data/src/incident-command/command-support-store.ts index 36a9f77e..1685a625 100644 --- a/packages/data/src/incident-command/command-support-store.ts +++ b/packages/data/src/incident-command/command-support-store.ts @@ -18,7 +18,8 @@ type SupportSlice = 'capabilities' | 'definitions' | 'voiceChannels'; interface CommandSupportData extends SliceMeta { /** * Effective `IncidentCapabilities` bitmask per incident (`IncidentRoles/GetMyCapabilities`): exactly the writes the server - * accepts from this member there, plus `ViewBoard` when they may read it. Absent = not loaded. + * accepts from this member there, plus `ViewBoard` when they may read it (`resolveCommandPermissions` gates both). + * Absent = not loaded. */ capabilitiesByCall: Record; /** Department command templates (`Commands/GetAllCommands`). */ @@ -62,7 +63,12 @@ export const useCommandSupportStore = create()((set, get) = 'capabilities', // Core puts the bitmask on the envelope (`Value`), not in `Data`; a member who may do nothing gets 0. async () => assertV4Success(await getMyIncidentCapabilities(ctx(), { callId: toCallParam(callId) })), - (result, state) => ({ capabilitiesByCall: { ...state.capabilitiesByCall, [callId]: typeof result?.Value === 'number' ? result.Value : 0 } }), + (result, state) => { + const value = result?.Value; + + // An answer without a bitmask says nothing: the capabilities stay unknown (the IC own-board fallback), not 0. + return typeof value === 'number' ? { capabilitiesByCall: { ...state.capabilitiesByCall, [callId]: value } } : {}; + }, { flightKey: `capabilities:${callId}`, trailing: options?.bypassCache === true } ); }, diff --git a/packages/features/src/incident-command/__tests__/command-board-view.test.tsx b/packages/features/src/incident-command/__tests__/command-board-view.test.tsx index c6167ebe..88db7af2 100644 --- a/packages/features/src/incident-command/__tests__/command-board-view.test.tsx +++ b/packages/features/src/incident-command/__tests__/command-board-view.test.tsx @@ -213,6 +213,14 @@ describe('CommandBoardView — command tier (IC)', () => { expect(screen.queryByTestId('lane-assign-n1')).toBeNull(); expect(screen.queryByTestId('command-end')).toBeNull(); }); + + it('keeps the member’s own board readable when a capability answer without ViewBoard is still current', async () => { + // e.g. read before their establish landed; Core counts the commander and establisher as on the incident. + useCommandSupportStore.setState({ capabilitiesByCall: { '7': IncidentCapabilities.None } }); + await render(); + expect(screen.getByTestId('command-board-view')).toBeTruthy(); + expect(screen.queryByTestId('command-board-not-viewable')).toBeNull(); + }); }); describe('CommandBoardView — command tier (IC), another member’s board', () => { @@ -273,6 +281,15 @@ describe('CommandBoardView — command tier (IC), another member’s board', () // Public notes need ManagePublicInformation. expect(screen.queryByTestId('note-sheet-visibility', hidden)).toBeNull(); }); + + it('hides a board the server says the member may not read, though the device still holds it', async () => { + useCommandSupportStore.setState({ capabilitiesByCall: { '7': IncidentCapabilities.None } }); + await render(); + expect(screen.getByTestId('command-board-not-viewable')).toBeTruthy(); + expect(screen.getByText('incident_command.board_not_viewable')).toBeTruthy(); + expect(screen.queryByTestId('command-board-view')).toBeNull(); + expect(screen.queryByText('Warehouse fire')).toBeNull(); + }); }); describe('CommandBoardView — assist tier (Dispatch)', () => { diff --git a/packages/features/src/incident-command/__tests__/incident-command-lib.test.ts b/packages/features/src/incident-command/__tests__/incident-command-lib.test.ts index 792b7808..d9bdc3c5 100644 --- a/packages/features/src/incident-command/__tests__/incident-command-lib.test.ts +++ b/packages/features/src/incident-command/__tests__/incident-command-lib.test.ts @@ -151,6 +151,17 @@ describe('permissions by tier', () => { expect(resolveCommandPermissions({ tier: 'command', isClosed: true, canEstablish: true, isOwnBoard: true })).toMatchObject({ canViewBoard: true, canManageCommand: false, canAssignResources: false }); }); + it('shows the board unless Core’s capabilities leave out ViewBoard (the member’s own board always)', () => { + for (const tier of ['assist', 'command'] as const) { + // Not loaded yet: the board read itself is Core's check. + expect(resolveCommandPermissions({ tier, isClosed: false, canEstablish: false }).canViewBoard).toBe(true); + expect(resolveCommandPermissions({ tier, capabilities: IncidentCapabilities.ViewBoard, isClosed: true, canEstablish: false }).canViewBoard).toBe(true); + // Core sends 0 to a member who may not read the board (no View Command Boards, not on the incident). + expect(resolveCommandPermissions({ tier, capabilities: IncidentCapabilities.None, isClosed: false, canEstablish: true })).toMatchObject({ canViewBoard: false, canManageCommand: false }); + expect(resolveCommandPermissions({ tier, capabilities: IncidentCapabilities.None, isClosed: false, canEstablish: true, isOwnBoard: true }).canViewBoard).toBe(true); + } + }); + it('each control maps to the capability Core checks for that write', () => { const safety = resolveCommandPermissions({ tier: 'command', diff --git a/packages/features/src/incident-command/components/command-board-view.tsx b/packages/features/src/incident-command/components/command-board-view.tsx index 25f55f02..d2312cfc 100644 --- a/packages/features/src/incident-command/components/command-board-view.tsx +++ b/packages/features/src/incident-command/components/command-board-view.tsx @@ -4,8 +4,8 @@ import { generateDataId, selectCallById, useCallsStore, useCommandStore, useComm import { callDisplayName } from '@resgrid/features/calls'; import { useTranslation } from '@resgrid/i18n'; import { type CommandStructureNode, type IncidentNeed, type IncidentTimer, type ResourceAssignment, type TacticalObjective } from '@resgrid/models'; -import { HStack, Text, useFormFactor, useToast } from '@resgrid/ui'; -import { Radio, ShieldCheck, Users } from 'lucide-react-native'; +import { HStack, Text, useFormFactor, useToast, ZeroState } from '@resgrid/ui'; +import { Radio, ShieldCheck, ShieldOff, Users } from 'lucide-react-native'; import React, { useCallback, useMemo, useState } from 'react'; import { type LayoutChangeEvent, StyleSheet, View } from 'react-native'; @@ -105,6 +105,7 @@ export const CommandBoardView: React.FC = ({ callId, tier const entry = useCommandStore((state) => state.boards[callId]); const refreshBoard = useCommandStore((state) => state.refreshBoard); const fetchTimeline = useCommandStore((state) => state.fetchTimeline); + const fetchCapabilities = useCommandSupportStore((state) => state.fetchCapabilities); const endCommand = useCommandStore((state) => state.endCommand); const call = useCallsStore((state) => selectCallById(state, callId)); const closeCapability = useCapability('calls').close; @@ -145,6 +146,12 @@ export const CommandBoardView: React.FC = ({ callId, tier void fetchTimeline(callId); }, [callId, fetchTimeline, refreshBoard]); + // Access to the board may have been granted since (View Command Boards, or a place on the incident): ask Core again. + const recheckAccess = useCallback(async () => { + await fetchCapabilities(callId, { bypassCache: true }); + await refreshBoard(callId, { bypassCache: true }); + }, [callId, fetchCapabilities, refreshBoard]); + // ---------------------------------------------------------------- resources const assignTo = useCallback( async (kind: number, resourceId: string, targetNodeId: string) => { @@ -222,6 +229,11 @@ export const CommandBoardView: React.FC = ({ callId, tier if (!entry) return null; + // Core says this member may not read the board (capabilities without ViewBoard): what the device still holds stays hidden. + if (!permissions.canViewBoard) { + return ; + } + const command = board?.Command ?? null; // The move needs the assign capability; a confirmation left open after losing it never shows. const visibleMoveConflict = permissions.canAssignResources ? moveConflict : null; diff --git a/packages/features/src/incident-command/i18n.en.json b/packages/features/src/incident-command/i18n.en.json index 554843e1..3d5c05ea 100644 --- a/packages/features/src/incident-command/i18n.en.json +++ b/packages/features/src/incident-command/i18n.en.json @@ -32,6 +32,8 @@ "incident_command.assistant_thinking": "Thinking…", "incident_command.assistant_title": "Incident assistant", "incident_command.assistant_unanswered": "Sorry, I couldn't answer that.", + "incident_command.board_not_viewable": "You can't view this command board", + "incident_command.board_not_viewable_description": "Seeing this board needs the View Command Boards permission or a place on the incident.", "incident_command.call_number": "Call #{{number}}", "incident_command.call_person": "Call {{name}}", "incident_command.chat_unavailable": "That chat isn't available for this incident.", diff --git a/packages/features/src/incident-command/lib/permissions.ts b/packages/features/src/incident-command/lib/permissions.ts index 57d4dc81..9edf3188 100644 --- a/packages/features/src/incident-command/lib/permissions.ts +++ b/packages/features/src/incident-command/lib/permissions.ts @@ -13,11 +13,14 @@ export type IncidentCommandTier = Capabilities['incidentCommand']; * command; ICS roles and command assist give parts; anyone else reads the board without changing it. * - Until the capabilities load (or offline), the IC tier treats a board the member commands, established, or started on * this device as fully theirs, and every other board as read-only; Dispatch is read-only. + * - Reading the board follows Core's `ViewBoard` bit (View Command Boards, or being on the incident): once the + * capabilities say the member may not read it, a board still held on the device is not shown. Until they load, the + * board read itself is the check (Core refuses it), and the member's own board is always readable. * Establishing needs the department's run-command permission (IC, `CanLoginToCommandApp`) or `CanCreateCalls` (Dispatch). * A closed command is read-only in every tier. */ export interface CommandPermissions { - /** The full board is shown (assist/command). */ + /** The full board is shown (assist/command, unless Core's capabilities leave out `ViewBoard`). Closed boards stay readable. */ canViewBoard: boolean; canEstablish: boolean; /** End, transfer, command details. */ @@ -77,7 +80,9 @@ export function resolveCommandPermissions({ tier, capabilities, isClosed, canEst const value = known ? capabilities : tier === 'command' && isOwnBoard ? IncidentCapabilities.All : IncidentCapabilities.None; const can = (flag: IncidentCapabilities) => !isClosed && hasIncidentCapability(value, flag); return { - canViewBoard: true, + // Core sends 0 to a member who may not read the board. The member's own board stays readable while a capability + // answer from before their establish landed is still current (Core counts commander and establisher as on the incident). + canViewBoard: !known || isOwnBoard || hasIncidentCapability(value, IncidentCapabilities.ViewBoard), canEstablish, canManageCommand: can(IncidentCapabilities.ManageCommand), canManageRoles: can(IncidentCapabilities.ManageCommand), diff --git a/packages/i18n/review/missing-translations.csv b/packages/i18n/review/missing-translations.csv index c518631a..6fa0252b 100644 --- a/packages/i18n/review/missing-translations.csv +++ b/packages/i18n/review/missing-translations.csv @@ -10690,3 +10690,21 @@ common,livekit.join_failed,it,"Could not join the voice channel. Please try agai common,livekit.join_failed,pl,"Could not join the voice channel. Please try again." common,livekit.join_failed,sv,"Could not join the voice channel. Please try again." common,livekit.join_failed,uk,"Could not join the voice channel. Please try again." +common,incident_command.board_not_viewable,ar,"You can't view this command board" +common,incident_command.board_not_viewable,de,"You can't view this command board" +common,incident_command.board_not_viewable,el,"You can't view this command board" +common,incident_command.board_not_viewable,es,"You can't view this command board" +common,incident_command.board_not_viewable,fr,"You can't view this command board" +common,incident_command.board_not_viewable,it,"You can't view this command board" +common,incident_command.board_not_viewable,pl,"You can't view this command board" +common,incident_command.board_not_viewable,sv,"You can't view this command board" +common,incident_command.board_not_viewable,uk,"You can't view this command board" +common,incident_command.board_not_viewable_description,ar,"Seeing this board needs the View Command Boards permission or a place on the incident." +common,incident_command.board_not_viewable_description,de,"Seeing this board needs the View Command Boards permission or a place on the incident." +common,incident_command.board_not_viewable_description,el,"Seeing this board needs the View Command Boards permission or a place on the incident." +common,incident_command.board_not_viewable_description,es,"Seeing this board needs the View Command Boards permission or a place on the incident." +common,incident_command.board_not_viewable_description,fr,"Seeing this board needs the View Command Boards permission or a place on the incident." +common,incident_command.board_not_viewable_description,it,"Seeing this board needs the View Command Boards permission or a place on the incident." +common,incident_command.board_not_viewable_description,pl,"Seeing this board needs the View Command Boards permission or a place on the incident." +common,incident_command.board_not_viewable_description,sv,"Seeing this board needs the View Command Boards permission or a place on the incident." +common,incident_command.board_not_viewable_description,uk,"Seeing this board needs the View Command Boards permission or a place on the incident." diff --git a/packages/i18n/review/summary.json b/packages/i18n/review/summary.json index 0466c50c..88a0bd10 100644 --- a/packages/i18n/review/summary.json +++ b/packages/i18n/review/summary.json @@ -1,7 +1,7 @@ { "rules": "see header of packages/i18n/scripts/merge-legacy-translations.mjs", "namespaces": { - "common": 3927, + "common": 3929, "responder": 754, "unit": 151, "ic": 698, diff --git a/packages/i18n/src/translations/common/ar.json b/packages/i18n/src/translations/common/ar.json index a8367efb..ba8b1144 100644 --- a/packages/i18n/src/translations/common/ar.json +++ b/packages/i18n/src/translations/common/ar.json @@ -1459,7 +1459,9 @@ "voice_preset_tactical": "Tactical", "work_time": "{{minutes}} minutes in lane", "write_failed": "That change could not be saved.", - "write_rejected": "The server refused that change. Review it in Pending changes." + "write_rejected": "The server refused that change. Review it in Pending changes.", + "board_not_viewable": "You can't view this command board", + "board_not_viewable_description": "Seeing this board needs the View Command Boards permission or a place on the incident." }, "livekit": { "audio_devices": "أجهزة الصوت", diff --git a/packages/i18n/src/translations/common/de.json b/packages/i18n/src/translations/common/de.json index fe6970cd..150b86dd 100644 --- a/packages/i18n/src/translations/common/de.json +++ b/packages/i18n/src/translations/common/de.json @@ -1459,7 +1459,9 @@ "voice_preset_tactical": "Tactical", "work_time": "{{minutes}} minutes in lane", "write_failed": "That change could not be saved.", - "write_rejected": "The server refused that change. Review it in Pending changes." + "write_rejected": "The server refused that change. Review it in Pending changes.", + "board_not_viewable": "You can't view this command board", + "board_not_viewable_description": "Seeing this board needs the View Command Boards permission or a place on the incident." }, "livekit": { "audio_devices": "Audiogeräte", diff --git a/packages/i18n/src/translations/common/el.json b/packages/i18n/src/translations/common/el.json index 342e1b5c..7d022772 100644 --- a/packages/i18n/src/translations/common/el.json +++ b/packages/i18n/src/translations/common/el.json @@ -1459,7 +1459,9 @@ "voice_preset_tactical": "Tactical", "work_time": "{{minutes}} minutes in lane", "write_failed": "That change could not be saved.", - "write_rejected": "The server refused that change. Review it in Pending changes." + "write_rejected": "The server refused that change. Review it in Pending changes.", + "board_not_viewable": "You can't view this command board", + "board_not_viewable_description": "Seeing this board needs the View Command Boards permission or a place on the incident." }, "livekit": { "audio_devices": "Συσκευές Ήχου", diff --git a/packages/i18n/src/translations/common/en.json b/packages/i18n/src/translations/common/en.json index 9f8aebb6..59374ecc 100644 --- a/packages/i18n/src/translations/common/en.json +++ b/packages/i18n/src/translations/common/en.json @@ -1459,7 +1459,9 @@ "voice_preset_tactical": "Tactical", "work_time": "{{minutes}} minutes in lane", "write_failed": "That change could not be saved.", - "write_rejected": "The server refused that change. Review it in Pending changes." + "write_rejected": "The server refused that change. Review it in Pending changes.", + "board_not_viewable": "You can't view this command board", + "board_not_viewable_description": "Seeing this board needs the View Command Boards permission or a place on the incident." }, "livekit": { "audio_devices": "Audio Devices", diff --git a/packages/i18n/src/translations/common/es.json b/packages/i18n/src/translations/common/es.json index 418e67c5..d3c7b6ce 100644 --- a/packages/i18n/src/translations/common/es.json +++ b/packages/i18n/src/translations/common/es.json @@ -1459,7 +1459,9 @@ "voice_preset_tactical": "Tactical", "work_time": "{{minutes}} minutes in lane", "write_failed": "That change could not be saved.", - "write_rejected": "The server refused that change. Review it in Pending changes." + "write_rejected": "The server refused that change. Review it in Pending changes.", + "board_not_viewable": "You can't view this command board", + "board_not_viewable_description": "Seeing this board needs the View Command Boards permission or a place on the incident." }, "livekit": { "audio_devices": "Dispositivos de audio", diff --git a/packages/i18n/src/translations/common/fr.json b/packages/i18n/src/translations/common/fr.json index 8cd0009b..35662649 100644 --- a/packages/i18n/src/translations/common/fr.json +++ b/packages/i18n/src/translations/common/fr.json @@ -1459,7 +1459,9 @@ "voice_preset_tactical": "Tactical", "work_time": "{{minutes}} minutes in lane", "write_failed": "That change could not be saved.", - "write_rejected": "The server refused that change. Review it in Pending changes." + "write_rejected": "The server refused that change. Review it in Pending changes.", + "board_not_viewable": "You can't view this command board", + "board_not_viewable_description": "Seeing this board needs the View Command Boards permission or a place on the incident." }, "livekit": { "audio_devices": "Appareils audio", diff --git a/packages/i18n/src/translations/common/it.json b/packages/i18n/src/translations/common/it.json index 3843e4d8..c3eaf444 100644 --- a/packages/i18n/src/translations/common/it.json +++ b/packages/i18n/src/translations/common/it.json @@ -1459,7 +1459,9 @@ "voice_preset_tactical": "Tactical", "work_time": "{{minutes}} minutes in lane", "write_failed": "That change could not be saved.", - "write_rejected": "The server refused that change. Review it in Pending changes." + "write_rejected": "The server refused that change. Review it in Pending changes.", + "board_not_viewable": "You can't view this command board", + "board_not_viewable_description": "Seeing this board needs the View Command Boards permission or a place on the incident." }, "livekit": { "audio_devices": "Dispositivi audio", diff --git a/packages/i18n/src/translations/common/pl.json b/packages/i18n/src/translations/common/pl.json index 251cdda4..5c0ec61b 100644 --- a/packages/i18n/src/translations/common/pl.json +++ b/packages/i18n/src/translations/common/pl.json @@ -1459,7 +1459,9 @@ "voice_preset_tactical": "Tactical", "work_time": "{{minutes}} minutes in lane", "write_failed": "That change could not be saved.", - "write_rejected": "The server refused that change. Review it in Pending changes." + "write_rejected": "The server refused that change. Review it in Pending changes.", + "board_not_viewable": "You can't view this command board", + "board_not_viewable_description": "Seeing this board needs the View Command Boards permission or a place on the incident." }, "livekit": { "audio_devices": "Urządzenia audio", diff --git a/packages/i18n/src/translations/common/sv.json b/packages/i18n/src/translations/common/sv.json index d0e9baf1..3ad9f10e 100644 --- a/packages/i18n/src/translations/common/sv.json +++ b/packages/i18n/src/translations/common/sv.json @@ -1459,7 +1459,9 @@ "voice_preset_tactical": "Tactical", "work_time": "{{minutes}} minutes in lane", "write_failed": "That change could not be saved.", - "write_rejected": "The server refused that change. Review it in Pending changes." + "write_rejected": "The server refused that change. Review it in Pending changes.", + "board_not_viewable": "You can't view this command board", + "board_not_viewable_description": "Seeing this board needs the View Command Boards permission or a place on the incident." }, "livekit": { "audio_devices": "Ljudenheter", diff --git a/packages/i18n/src/translations/common/uk.json b/packages/i18n/src/translations/common/uk.json index 169c969d..0e87979b 100644 --- a/packages/i18n/src/translations/common/uk.json +++ b/packages/i18n/src/translations/common/uk.json @@ -1459,7 +1459,9 @@ "voice_preset_tactical": "Tactical", "work_time": "{{minutes}} minutes in lane", "write_failed": "That change could not be saved.", - "write_rejected": "The server refused that change. Review it in Pending changes." + "write_rejected": "The server refused that change. Review it in Pending changes.", + "board_not_viewable": "You can't view this command board", + "board_not_viewable_description": "Seeing this board needs the View Command Boards permission or a place on the incident." }, "livekit": { "audio_devices": "Аудіопристрої",