Skip to content

Commit 5438177

Browse files
author
Mohammod Al Amin Ashik
committed
fix: validate activeSpaceId on load and scope connected count per space
Two bugs fixed: 1. appStore.setSpaces now validates that the persisted activeSpaceId still exists in the loaded spaces list. Previously it only checked for null/undefined, so a deleted space ID (e.g. from e2e tests) would persist and cause viewSpace to resolve to null, making the entire "My Servers" page and dashboard installed count show empty. 2. Gateway connected_backends count is now scoped to the viewing space. Added ServerManager.connected_count_for_space() and passed space_id through get_gateway_status command so Dashboard and ServersPage show the correct connected count for the space being viewed.
1 parent d720ef0 commit 5438177

7 files changed

Lines changed: 78 additions & 11 deletions

File tree

apps/desktop/src-tauri/src/commands/gateway.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -504,9 +504,10 @@ fn create_gateway_dependencies(
504504
builder.build().map_err(|e: String| e)
505505
}
506506

507-
/// Get gateway status
507+
/// Get gateway status, optionally scoped to a specific space
508508
#[tauri::command]
509509
pub async fn get_gateway_status(
510+
space_id: Option<String>,
510511
gateway_state: State<'_, Arc<RwLock<GatewayAppState>>>,
511512
server_manager_state: State<'_, Arc<RwLock<ServerManagerState>>>,
512513
) -> Result<GatewayStatus, String> {
@@ -519,19 +520,24 @@ pub async fn get_gateway_status(
519520
0
520521
};
521522

522-
// Get connected count from ServerManager
523+
// Get connected count from ServerManager, scoped to space if provided
523524
let connected_backends = {
524525
let sm_state = server_manager_state.read().await;
525526
if let Some(ref manager) = sm_state.manager {
526-
manager.connected_count().await
527+
if let Some(ref sid) = space_id {
528+
let uuid = Uuid::parse_str(sid).map_err(|e| e.to_string())?;
529+
manager.connected_count_for_space(&uuid).await
530+
} else {
531+
manager.connected_count().await
532+
}
527533
} else {
528534
0
529535
}
530536
};
531537

532538
info!(
533-
"[Gateway] get_gateway_status: running={}, url={:?}, sessions={}, backends={}",
534-
state.running, state.url, active_sessions, connected_backends
539+
"[Gateway] get_gateway_status: running={}, url={:?}, sessions={}, backends={}, space={:?}",
540+
state.running, state.url, active_sessions, connected_backends, space_id
535541
);
536542

537543
Ok(GatewayStatus {

apps/desktop/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ function DashboardView() {
230230
import('@/lib/api/featureSets').then((m) =>
231231
viewSpace?.id ? m.listFeatureSetsBySpace(viewSpace.id) : m.listFeatureSets()
232232
),
233-
import('@/lib/api/gateway').then((m) => m.getGatewayStatus()),
233+
import('@/lib/api/gateway').then((m) => m.getGatewayStatus(viewSpace?.id)),
234234
import('@/lib/api/registry').then((m) => m.listInstalledServers(viewSpace?.id)),
235235
]);
236236
console.log('[Dashboard] Gateway status received:', gateway);

apps/desktop/src/features/servers/ServersPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ export function ServersPage() {
273273
// Use allSettled so we can show installed servers even if registry is offline
274274
const [installedResult, gatewayResult, definitionsResult] = await Promise.allSettled([
275275
import('@/lib/api/registry').then((m) => m.listInstalledServers(viewSpace?.id)),
276-
import('@/lib/api/gateway').then((m) => m.getGatewayStatus()),
276+
import('@/lib/api/gateway').then((m) => m.getGatewayStatus(viewSpace?.id)),
277277
import('@/lib/api/registry').then((m) => m.discoverServers()),
278278
]);
279279

apps/desktop/src/lib/api/gateway.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ export type ExportFormat = 'cursor' | 'vscode' | 'claude';
1818
/**
1919
* Get gateway status.
2020
*/
21-
export async function getGatewayStatus(): Promise<GatewayStatus> {
22-
return invoke('get_gateway_status');
21+
export async function getGatewayStatus(spaceId?: string): Promise<GatewayStatus> {
22+
return invoke('get_gateway_status', { spaceId });
2323
}
2424

2525
/**

apps/desktop/src/stores/appStore.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,11 @@ export const useAppStore = create<AppStore>()(
2424
setSpaces: (spaces) =>
2525
set((state) => {
2626
state.spaces = spaces;
27-
// Auto-select active space if none selected
28-
if (!state.activeSpaceId && spaces.length > 0) {
27+
// Validate persisted activeSpaceId still exists, reset to default if not
28+
const activeExists = state.activeSpaceId
29+
? spaces.some((s) => s.id === state.activeSpaceId)
30+
: false;
31+
if (!activeExists && spaces.length > 0) {
2932
const defaultSpace = spaces.find((s) => s.is_default);
3033
state.activeSpaceId = defaultSpace?.id ?? spaces[0].id;
3134
}

crates/mcpmux-gateway/src/pool/server_manager.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,20 @@ impl ServerManager {
280280
count
281281
}
282282

283+
/// Count connected servers for a specific space
284+
pub async fn connected_count_for_space(&self, space_id: &Uuid) -> usize {
285+
let mut count = 0;
286+
for entry in self.states.iter() {
287+
if &entry.key().space_id == space_id {
288+
let state = entry.value().read().await;
289+
if state.status == ConnectionStatus::Connected {
290+
count += 1;
291+
}
292+
}
293+
}
294+
count
295+
}
296+
283297
/// Emit a domain event (unified event system)
284298
fn emit(&self, event: DomainEvent) {
285299
// Trace Refreshing events to find the source

tests/ts/stores/appStore.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,50 @@ describe('appStore', () => {
5555

5656
expect(useAppStore.getState().viewSpaceId).toBe(useAppStore.getState().activeSpaceId);
5757
});
58+
59+
it('should reset activeSpaceId when persisted value points to deleted space', () => {
60+
const spaces = [
61+
createTestSpace({ name: 'Space A', is_default: false }),
62+
createDefaultSpace({ name: 'Default Space' }),
63+
];
64+
// Simulate a persisted activeSpaceId that no longer exists in the spaces list
65+
useAppStore.setState({ activeSpaceId: 'deleted-space-id' });
66+
useAppStore.getState().setSpaces(spaces);
67+
68+
// Should fallback to the default space
69+
expect(useAppStore.getState().activeSpaceId).toBe(spaces[1].id);
70+
});
71+
72+
it('should reset activeSpaceId to first space when no default exists', () => {
73+
const spaces = [
74+
createTestSpace({ name: 'Space A', is_default: false }),
75+
createTestSpace({ name: 'Space B', is_default: false }),
76+
];
77+
useAppStore.setState({ activeSpaceId: 'deleted-space-id' });
78+
useAppStore.getState().setSpaces(spaces);
79+
80+
expect(useAppStore.getState().activeSpaceId).toBe(spaces[0].id);
81+
});
82+
83+
it('should keep activeSpaceId when it still exists in spaces list', () => {
84+
const spaces = createTestSpaces(3);
85+
useAppStore.setState({ activeSpaceId: spaces[1].id });
86+
useAppStore.getState().setSpaces(spaces);
87+
88+
expect(useAppStore.getState().activeSpaceId).toBe(spaces[1].id);
89+
});
90+
91+
it('should reset both activeSpaceId and viewSpaceId when both point to deleted spaces', () => {
92+
const spaces = [createDefaultSpace({ name: 'My Space' })];
93+
useAppStore.setState({
94+
activeSpaceId: 'deleted-active-id',
95+
viewSpaceId: 'deleted-view-id',
96+
});
97+
useAppStore.getState().setSpaces(spaces);
98+
99+
expect(useAppStore.getState().activeSpaceId).toBe(spaces[0].id);
100+
expect(useAppStore.getState().viewSpaceId).toBe(spaces[0].id);
101+
});
58102
});
59103

60104
describe('setActiveSpace', () => {

0 commit comments

Comments
 (0)