diff --git a/chat-frontend/src/components/MainApp/AppHeader/SearchBar/SearchBar.test.jsx b/chat-frontend/src/components/MainApp/AppHeader/SearchBar/SearchBar.test.jsx
index d8c4ede98..944793ec8 100644
--- a/chat-frontend/src/components/MainApp/AppHeader/SearchBar/SearchBar.test.jsx
+++ b/chat-frontend/src/components/MainApp/AppHeader/SearchBar/SearchBar.test.jsx
@@ -34,8 +34,8 @@ describe('SearchBar', () => {
it('fetches rooms after 250ms debounce when query >= 2 chars', async () => {
const request = vi.fn().mockResolvedValue({
- results: [
- { roomId: 'r1', roomName: 'general', roomType: 'c', siteId: 'site-A' },
+ rooms: [
+ { roomId: 'r1', name: 'general', roomType: 'c', siteId: 'site-A' },
],
total: 1,
})
@@ -53,16 +53,16 @@ describe('SearchBar', () => {
await waitFor(() => {
expect(request).toHaveBeenCalledWith(
'chat.user.alice.request.search.rooms',
- { searchText: 'fro', scope: 'all', size: 8 }
+ { query: 'fro', roomType: 'all', size: 8 }
)
})
})
it('shows results in dropdown', async () => {
const request = vi.fn().mockResolvedValue({
- results: [
- { roomId: 'r1', roomName: 'frontend-team', roomType: 'c', siteId: 'site-A' },
- { roomId: 'r2', roomName: 'frontend-perf', roomType: 'c', siteId: 'site-A' },
+ rooms: [
+ { roomId: 'r1', name: 'frontend-team', roomType: 'c', siteId: 'site-A' },
+ { roomId: 'r2', name: 'frontend-perf', roomType: 'c', siteId: 'site-A' },
],
total: 2,
})
@@ -84,8 +84,8 @@ describe('SearchBar', () => {
it('clicking result calls onSelectRoom and clears input', async () => {
const onSelectRoom = vi.fn()
const request = vi.fn().mockResolvedValue({
- results: [
- { roomId: 'r1', roomName: 'general', roomType: 'c', siteId: 'site-A' },
+ rooms: [
+ { roomId: 'r1', name: 'general', roomType: 'c', siteId: 'site-A' },
],
total: 1,
})
@@ -115,7 +115,7 @@ describe('SearchBar', () => {
const onEnterSearch = vi.fn()
useNats.mockReturnValue({
user: { account: 'alice' },
- request: vi.fn().mockResolvedValue({ results: [], total: 0 }),
+ request: vi.fn().mockResolvedValue({ rooms: [], total: 0 }),
})
render(
)
@@ -131,7 +131,7 @@ describe('SearchBar', () => {
useNats.mockReturnValue({
user: { account: 'alice' },
request: vi.fn().mockResolvedValue({
- results: [{ roomId: 'r1', roomName: 'general', roomType: 'c', siteId: 'site-A' }],
+ rooms: [{ roomId: 'r1', name: 'general', roomType: 'c', siteId: 'site-A' }],
total: 1,
}),
})
diff --git a/chat-frontend/src/components/MainApp/ChatPage/InRoomSearch/InRoomSearch.jsx b/chat-frontend/src/components/MainApp/ChatPage/InRoomSearch/InRoomSearch.jsx
index 45e5c8b43..5837cb2fb 100644
--- a/chat-frontend/src/components/MainApp/ChatPage/InRoomSearch/InRoomSearch.jsx
+++ b/chat-frontend/src/components/MainApp/ChatPage/InRoomSearch/InRoomSearch.jsx
@@ -29,7 +29,7 @@ export default function InRoomSearch({ roomId, onClose, onJumpToMessage }) {
setSubmitted(true)
try {
const resp = await searchMessages(nats, { searchText: q, roomIds: [roomId], size: 50 })
- setResults(resp.results ?? [])
+ setResults(resp.messages ?? [])
} catch {
setResults([])
} finally {
diff --git a/chat-frontend/src/components/MainApp/ChatPage/InRoomSearch/InRoomSearch.test.jsx b/chat-frontend/src/components/MainApp/ChatPage/InRoomSearch/InRoomSearch.test.jsx
index a99ef15a9..d7b1d4556 100644
--- a/chat-frontend/src/components/MainApp/ChatPage/InRoomSearch/InRoomSearch.test.jsx
+++ b/chat-frontend/src/components/MainApp/ChatPage/InRoomSearch/InRoomSearch.test.jsx
@@ -14,7 +14,7 @@ describe('InRoomSearch', () => {
})
it('does not fetch on typing; fetches on Enter scoped to roomIds: [roomId]', async () => {
- const request = vi.fn().mockResolvedValue({ results: [], total: 0 })
+ const request = vi.fn().mockResolvedValue({ messages: [], total: 0 })
useNats.mockReturnValue({
user: { account: 'alice' },
request,
@@ -38,7 +38,7 @@ describe('InRoomSearch', () => {
await waitFor(() => {
expect(request).toHaveBeenCalledWith(
'chat.user.alice.request.search.messages',
- { searchText: 'hi', roomIds: ['r1'], size: 50 }
+ { query: 'hi', roomIds: ['r1'], size: 50 }
)
})
})
@@ -47,7 +47,7 @@ describe('InRoomSearch', () => {
const onJumpToMessage = vi.fn()
const onClose = vi.fn()
const request = vi.fn().mockResolvedValue({
- results: [
+ messages: [
{ messageId: 'msg-1', roomId: 'r1', content: 'hello world' },
],
total: 1,
@@ -83,7 +83,7 @@ describe('InRoomSearch', () => {
const onClose = vi.fn()
useNats.mockReturnValue({
user: { account: 'alice' },
- request: vi.fn().mockResolvedValue({ results: [], total: 0 }),
+ request: vi.fn().mockResolvedValue({ messages: [], total: 0 }),
})
render(
diff --git a/chat-frontend/src/components/MainApp/ChatPage/ManageMembersDialog/MemberPicker/MemberPicker.jsx b/chat-frontend/src/components/MainApp/ChatPage/ManageMembersDialog/MemberPicker/MemberPicker.jsx
index b0b24e666..a973423a1 100644
--- a/chat-frontend/src/components/MainApp/ChatPage/ManageMembersDialog/MemberPicker/MemberPicker.jsx
+++ b/chat-frontend/src/components/MainApp/ChatPage/ManageMembersDialog/MemberPicker/MemberPicker.jsx
@@ -23,7 +23,7 @@ const identity = (e) => e
const renderChannelResult = (r) => (
- {r.roomName}
+ {r.name}
— {r.siteId}
)
@@ -78,8 +78,8 @@ const MemberPicker = forwardRef(function MemberPicker(
const channelFetcher = useCallback(
async (q) => {
- const resp = await searchRooms(nats, { searchText: q, scope: 'all', size: 8 })
- return resp.results ?? []
+ const resp = await searchRooms(nats, { searchText: q, roomType: 'all', size: 8 })
+ return resp.rooms ?? []
},
[nats]
)
diff --git a/chat-frontend/src/components/MainApp/ChatPage/ManageMembersDialog/MemberPicker/MemberPicker.test.jsx b/chat-frontend/src/components/MainApp/ChatPage/ManageMembersDialog/MemberPicker/MemberPicker.test.jsx
index bda7a55de..8988b22cc 100644
--- a/chat-frontend/src/components/MainApp/ChatPage/ManageMembersDialog/MemberPicker/MemberPicker.test.jsx
+++ b/chat-frontend/src/components/MainApp/ChatPage/ManageMembersDialog/MemberPicker/MemberPicker.test.jsx
@@ -10,7 +10,7 @@ vi.mock('@/context/NatsContext', () => ({
import { useNats } from '@/context/NatsContext'
function setup(overrides = {}) {
- const request = vi.fn().mockResolvedValue({ results: [] })
+ const request = vi.fn().mockResolvedValue({ rooms: [] })
useNats.mockReturnValue({
user: { account: 'alice', siteId: 'site-A' },
request,
@@ -206,7 +206,7 @@ describe('MemberPicker', () => {
it('debounces search.rooms (channels) and adds a ChannelRef when a result is clicked', async () => {
const request = vi.fn().mockResolvedValue({
- results: [{ roomId: 'r-x', roomName: 'project-x', siteId: 'site-B', roomType: 'c' }],
+ rooms: [{ roomId: 'r-x', name: 'project-x', siteId: 'site-B', roomType: 'c' }],
})
const onChannelsChange = vi.fn()
useNats.mockReturnValue({ user: { account: 'alice', siteId: 'site-A' }, request })
@@ -225,7 +225,7 @@ describe('MemberPicker', () => {
await waitFor(() => {
expect(request).toHaveBeenCalledWith(
'chat.user.alice.request.search.rooms',
- expect.objectContaining({ searchText: 'pro' })
+ expect.objectContaining({ query: 'pro' })
)
})
await waitFor(() => expect(screen.getByText('project-x')).toBeInTheDocument())
diff --git a/chat-frontend/src/components/MainApp/SearchResultsPane/SearchResultsPane.jsx b/chat-frontend/src/components/MainApp/SearchResultsPane/SearchResultsPane.jsx
index 3d4083f36..b3bf01efa 100644
--- a/chat-frontend/src/components/MainApp/SearchResultsPane/SearchResultsPane.jsx
+++ b/chat-frontend/src/components/MainApp/SearchResultsPane/SearchResultsPane.jsx
@@ -31,11 +31,12 @@ export default function SearchResultsPane({
setRoomsError(null)
setMsgsError(null)
- searchRooms(nats, { searchText: query, scope: 'all', size: 50 })
+ searchRooms(nats, { searchText: query, roomType: 'all', size: 50 })
.then((resp) => {
if (cancelled) return
- setRoomResults(resp.results ?? [])
- setRoomTotal(resp.total ?? 0)
+ const rooms = resp.rooms ?? []
+ setRoomResults(rooms)
+ setRoomTotal(rooms.length)
})
.catch((err) => {
if (!cancelled) setRoomsError(err?.message || 'Search failed')
@@ -47,7 +48,7 @@ export default function SearchResultsPane({
searchMessages(nats, { searchText: query, size: 50 })
.then((resp) => {
if (cancelled) return
- setMsgResults(resp.results ?? [])
+ setMsgResults(resp.messages ?? [])
setMsgTotal(resp.total ?? 0)
})
.catch((err) => {
@@ -120,7 +121,7 @@ export default function SearchResultsPane({
{searchRoomPrefix(hit.roomType)}
-
{hit.roomName}
+
{hit.name}
))}
diff --git a/chat-frontend/src/components/MainApp/SearchResultsPane/SearchResultsPane.test.jsx b/chat-frontend/src/components/MainApp/SearchResultsPane/SearchResultsPane.test.jsx
index c2856efaf..cce308163 100644
--- a/chat-frontend/src/components/MainApp/SearchResultsPane/SearchResultsPane.test.jsx
+++ b/chat-frontend/src/components/MainApp/SearchResultsPane/SearchResultsPane.test.jsx
@@ -15,10 +15,9 @@ describe('SearchResultsPane', () => {
it('fetches and displays room results immediately', async () => {
const request = vi.fn().mockResolvedValue({
- results: [
- { roomId: 'r1', roomName: 'general', roomType: 'c', siteId: 'site-A' },
+ rooms: [
+ { roomId: 'r1', name: 'general', roomType: 'c', siteId: 'site-A' },
],
- total: 1,
})
useNats.mockReturnValue({
user: { account: 'alice' },
@@ -40,7 +39,7 @@ describe('SearchResultsPane', () => {
expect(request).toHaveBeenCalledWith(
'chat.user.alice.request.search.rooms',
- expect.objectContaining({ searchText: 'gen' })
+ { query: 'gen', roomType: 'all', size: 50 }
)
})
@@ -48,13 +47,12 @@ describe('SearchResultsPane', () => {
const request = vi.fn().mockImplementation((subject) => {
if (subject.includes('.search.rooms')) {
return Promise.resolve({
- results: [{ roomId: 'r1', roomName: 'general', roomType: 'c', siteId: 'site-A' }],
- total: 1,
+ rooms: [{ roomId: 'r1', name: 'general', roomType: 'c', siteId: 'site-A' }],
})
}
if (subject.includes('.search.messages')) {
return Promise.resolve({
- results: [
+ messages: [
{ messageId: 'm1', roomId: 'r1', content: 'hello', createdAt: '2026-04-17T10:00:00Z', userAccount: 'bob' },
],
total: 1,
@@ -93,10 +91,9 @@ describe('SearchResultsPane', () => {
const onSelectRoom = vi.fn()
const onClose = vi.fn()
const request = vi.fn().mockResolvedValue({
- results: [
- { roomId: 'r1', roomName: 'general', roomType: 'c', siteId: 'site-A' },
+ rooms: [
+ { roomId: 'r1', name: 'general', roomType: 'c', siteId: 'site-A' },
],
- total: 1,
})
useNats.mockReturnValue({
user: { account: 'alice' },
@@ -130,11 +127,11 @@ describe('SearchResultsPane', () => {
const onClose = vi.fn()
const request = vi.fn().mockImplementation((subject) => {
if (subject.includes('.search.rooms')) {
- return Promise.resolve({ results: [], total: 0 })
+ return Promise.resolve({ rooms: [] })
}
if (subject.includes('.search.messages')) {
return Promise.resolve({
- results: [
+ messages: [
{ messageId: 'm1', roomId: 'r1', content: 'hello world', createdAt: '2026-04-17T10:00:00Z', userAccount: 'bob' },
],
total: 1,
diff --git a/chat-frontend/src/lib/roomFormat.js b/chat-frontend/src/lib/roomFormat.js
index db20bbad3..efcb55c8b 100644
--- a/chat-frontend/src/lib/roomFormat.js
+++ b/chat-frontend/src/lib/roomFormat.js
@@ -31,7 +31,7 @@ export function roomDisplayName(room) {
export function roomFromSearchHit(hit) {
return {
id: hit.roomId,
- name: hit.roomName,
+ name: hit.name,
type: hit.roomType,
siteId: hit.siteId,
}
diff --git a/chat-frontend/src/lib/roomFormat.test.js b/chat-frontend/src/lib/roomFormat.test.js
index 1fe75c652..1c7b302ec 100644
--- a/chat-frontend/src/lib/roomFormat.test.js
+++ b/chat-frontend/src/lib/roomFormat.test.js
@@ -78,7 +78,7 @@ describe('roomDisplayName', () => {
describe('roomFromSearchHit', () => {
it('maps search-hit field names onto the room shape', () => {
- const hit = { roomId: 'r1', roomName: 'frontend', roomType: 'channel', siteId: 'site-A' }
+ const hit = { roomId: 'r1', name: 'frontend', roomType: 'channel', siteId: 'site-A' }
expect(roomFromSearchHit(hit)).toEqual({
id: 'r1',
name: 'frontend',
diff --git a/docker-local/setup.sh b/docker-local/setup.sh
index 3fd0e8fba..98ed6478e 100755
--- a/docker-local/setup.sh
+++ b/docker-local/setup.sh
@@ -68,7 +68,11 @@ docker run --rm \
'
cp "$TMPDIR/backend.creds" "$BACKEND_CREDS"
-chmod 600 "$BACKEND_CREDS"
+# 0644, not 0600: service containers run as non-root (uid 10001) and
+# bind-mount this file read-only at /etc/nats/backend.creds, so the
+# in-container user must be able to read it. Acceptable only because
+# this is a throwaway local-dev credential generated by this script.
+chmod 644 "$BACKEND_CREDS"
OPERATOR_JWT=$(cat "$TMPDIR/operator.jwt")
ACCOUNT_JWT=$(cat "$TMPDIR/account.jwt")
diff --git a/docs/client-api.md b/docs/client-api.md
index dae218651..fa17fb056 100644
--- a/docs/client-api.md
+++ b/docs/client-api.md
@@ -1575,7 +1575,7 @@ See [Error envelope](#6-error-envelope-reference).
**Subject:** `chat.user.{account}.request.search.rooms`
**Reply subject:** auto-generated `_INBOX.>` (NATS request/reply)
-Full-text search across rooms the requester is subscribed to. Results are returned as `SearchRoom` projections hydrated from MongoDB (the caller's per-room subscription documents), not raw ES index fields.
+Full-text search across rooms the requester is subscribed to. Results are served directly from the spotlight ES index (one document per `(account, room)` pair), in ES relevance order.
##### Request body
@@ -1600,13 +1600,14 @@ Full-text search across rooms the requester is subscribed to. Results are return
|---------|-------------------|-------|
| `rooms` | array