mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-30 12:09:15 +00:00
fix(console): clarify Pilot Agent host-console unavailability (#1868)
Replace the generic upgrade message on Pilot Agent nodes with transport-specific copy while preserving DAP-remote behavior. - Add resolveHostConsoleLockMessage() to return Pilot-specific copy when nodeMode === 'pilot_agent'; otherwise keep generic upgrade message. - Update ViewRouter to use the helper in the locked branch. - Add unit tests for the new helper and component behavior. - Sync docs/features/host-console.mdx to reflect the Pilot Agent message. Closes #1855
This commit is contained in:
@@ -113,6 +113,6 @@ The Host Console is one of the most powerful features in Sencho and is treated a
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
<Accordion title="Console tab is not visible">
|
<Accordion title="Console tab is not visible">
|
||||||
Console appears for users with the **admin** role. If you are an admin and still do not see it, refresh the page. On a remote node, the Console tab may show a lock card when that node does not support Host Console (for example a Pilot Agent node, or a Distributed API Proxy node that does not advertise Host Console).
|
Console appears for users with the **admin** role. If you are an admin and still do not see it, refresh the page. On a remote node, the Console tab may show a lock card when that node does not support Host Console. For a Pilot Agent node, the card states that Host Console is not available through Pilot Agent yet. For a Distributed API Proxy node that does not advertise Host Console, the card names the node's version and suggests upgrading it.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</AccordionGroup>
|
</AccordionGroup>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Skeleton } from '@/components/ui/skeleton';
|
|||||||
import { useAuth } from '@/context/AuthContext';
|
import { useAuth } from '@/context/AuthContext';
|
||||||
import { useLicense } from '@/context/LicenseContext';
|
import { useLicense } from '@/context/LicenseContext';
|
||||||
import { useNodes } from '@/context/NodeContext';
|
import { useNodes } from '@/context/NodeContext';
|
||||||
import { resolveHostConsoleCapability } from '@/lib/routing/hostConsoleCapability';
|
import { resolveHostConsoleCapability, resolveHostConsoleLockMessage } from '@/lib/routing/hostConsoleCapability';
|
||||||
import { LockCard } from '../ui/LockCard';
|
import { LockCard } from '../ui/LockCard';
|
||||||
import { CapabilityGate } from '../CapabilityGate';
|
import { CapabilityGate } from '../CapabilityGate';
|
||||||
import { HubOnlyGate } from '../HubOnlyGate';
|
import { HubOnlyGate } from '../HubOnlyGate';
|
||||||
@@ -207,17 +207,16 @@ export function ViewRouter({
|
|||||||
});
|
});
|
||||||
if (capState === 'loading') return <ViewSkeleton />;
|
if (capState === 'loading') return <ViewSkeleton />;
|
||||||
if (capState === 'locked') {
|
if (capState === 'locked') {
|
||||||
const nodeName = activeNode.name;
|
const { title, body } = resolveHostConsoleLockMessage({
|
||||||
const version = activeNodeMeta?.version;
|
nodeMode: activeNode.mode,
|
||||||
let versionHint = `${nodeName} does not advertise this capability.`;
|
nodeName: activeNode.name,
|
||||||
if (version && version !== 'unknown' && version !== '0.0.0-dev') {
|
version: activeNodeMeta?.version,
|
||||||
versionHint = `${nodeName} is running v${version}.`;
|
});
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<LockCard
|
<LockCard
|
||||||
icon={Unplug}
|
icon={Unplug}
|
||||||
title="Host Console is not available on this node"
|
title={title}
|
||||||
body={`${versionHint} Upgrade the node to use this feature.`}
|
body={body}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,12 +128,25 @@ describe('ViewRouter host-console', () => {
|
|||||||
|
|
||||||
it('shows a lock card for Community + legacy remote without mounting HostConsole', () => {
|
it('shows a lock card for Community + legacy remote without mounting HostConsole', () => {
|
||||||
vi.mocked(NodeContext.useNodes).mockReturnValue({
|
vi.mocked(NodeContext.useNodes).mockReturnValue({
|
||||||
activeNode: { id: 2, name: 'Legacy', type: 'remote' },
|
activeNode: { id: 2, name: 'Legacy', type: 'remote', mode: 'proxy' },
|
||||||
activeNodeMeta: { version: '0.95.0', capabilities: ['host-console'], fetchedAt: 1 },
|
activeNodeMeta: { version: '0.95.0', capabilities: ['host-console'], fetchedAt: 1 },
|
||||||
} as unknown as ReturnType<typeof NodeContext.useNodes>);
|
} as unknown as ReturnType<typeof NodeContext.useNodes>);
|
||||||
render(<ViewRouter {...baseProps} />);
|
render(<ViewRouter {...baseProps} />);
|
||||||
expect(screen.queryByTestId('host-console')).toBeNull();
|
expect(screen.queryByTestId('host-console')).toBeNull();
|
||||||
expect(screen.getByText(/Host Console is not available on this node/i)).toBeTruthy();
|
expect(screen.getByText(/Host Console is not available on this node/i)).toBeTruthy();
|
||||||
|
expect(screen.getByText(/Legacy is running v0\.95\.0\. Upgrade the node to use this feature\./i)).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows Pilot-specific copy for a pilot_agent node without mounting HostConsole', () => {
|
||||||
|
vi.mocked(NodeContext.useNodes).mockReturnValue({
|
||||||
|
activeNode: { id: 4, name: 'Pilot', type: 'remote', mode: 'pilot_agent' },
|
||||||
|
activeNodeMeta: { version: '0.97.1', capabilities: [], fetchedAt: 1 },
|
||||||
|
} as unknown as ReturnType<typeof NodeContext.useNodes>);
|
||||||
|
render(<ViewRouter {...baseProps} />);
|
||||||
|
expect(screen.queryByTestId('host-console')).toBeNull();
|
||||||
|
expect(screen.getByText(/Host Console is not available through Pilot Agent yet/i)).toBeTruthy();
|
||||||
|
expect(screen.getByText(/Host Console is currently available on the local node and Distributed API Proxy remotes\./i)).toBeTruthy();
|
||||||
|
expect(screen.queryByText(/Upgrade the node to use this feature\./i)).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('mounts Host Console for Admiral + legacy remote after meta resolves', async () => {
|
it('mounts Host Console for Admiral + legacy remote after meta resolves', async () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { resolveHostConsoleCapability } from './hostConsoleCapability';
|
import { resolveHostConsoleCapability, resolveHostConsoleLockMessage } from './hostConsoleCapability';
|
||||||
|
|
||||||
describe('resolveHostConsoleCapability', () => {
|
describe('resolveHostConsoleCapability', () => {
|
||||||
it('returns loading when the active node is unresolved', () => {
|
it('returns loading when the active node is unresolved', () => {
|
||||||
@@ -92,3 +92,60 @@ describe('resolveHostConsoleCapability', () => {
|
|||||||
})).toBe('locked');
|
})).toBe('locked');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('resolveHostConsoleLockMessage', () => {
|
||||||
|
it('returns Pilot-specific copy for a pilot_agent node regardless of version', () => {
|
||||||
|
expect(resolveHostConsoleLockMessage({
|
||||||
|
nodeMode: 'pilot_agent',
|
||||||
|
nodeName: 'Pilot',
|
||||||
|
version: '0.97.1',
|
||||||
|
})).toEqual({
|
||||||
|
title: 'Host Console is not available through Pilot Agent yet',
|
||||||
|
body: 'Host Console is currently available on the local node and Distributed API Proxy remotes.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the upgrade copy for a proxy node with a real version', () => {
|
||||||
|
expect(resolveHostConsoleLockMessage({
|
||||||
|
nodeMode: 'proxy',
|
||||||
|
nodeName: 'Peer',
|
||||||
|
version: '0.95.0',
|
||||||
|
})).toEqual({
|
||||||
|
title: 'Host Console is not available on this node',
|
||||||
|
body: 'Peer is running v0.95.0. Upgrade the node to use this feature.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the no-capability copy for a proxy node with a placeholder version', () => {
|
||||||
|
expect(resolveHostConsoleLockMessage({
|
||||||
|
nodeMode: 'proxy',
|
||||||
|
nodeName: 'Peer',
|
||||||
|
version: '0.0.0-dev',
|
||||||
|
})).toEqual({
|
||||||
|
title: 'Host Console is not available on this node',
|
||||||
|
body: 'Peer does not advertise this capability. Upgrade the node to use this feature.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the no-capability copy for a proxy node with an unknown version', () => {
|
||||||
|
expect(resolveHostConsoleLockMessage({
|
||||||
|
nodeMode: 'proxy',
|
||||||
|
nodeName: 'Peer',
|
||||||
|
version: 'unknown',
|
||||||
|
})).toEqual({
|
||||||
|
title: 'Host Console is not available on this node',
|
||||||
|
body: 'Peer does not advertise this capability. Upgrade the node to use this feature.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('treats an undefined mode as the generic proxy fallback', () => {
|
||||||
|
expect(resolveHostConsoleLockMessage({
|
||||||
|
nodeMode: undefined,
|
||||||
|
nodeName: 'Peer',
|
||||||
|
version: null,
|
||||||
|
})).toEqual({
|
||||||
|
title: 'Host Console is not available on this node',
|
||||||
|
body: 'Peer does not advertise this capability. Upgrade the node to use this feature.',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import {
|
|||||||
HOST_CONSOLE_CAPABILITY,
|
HOST_CONSOLE_CAPABILITY,
|
||||||
HOST_CONSOLE_COMMUNITY_CAPABILITY,
|
HOST_CONSOLE_COMMUNITY_CAPABILITY,
|
||||||
} from '@/lib/capabilities';
|
} from '@/lib/capabilities';
|
||||||
|
import type { NodeMode } from '@/context/NodeContext';
|
||||||
|
import { formatVersion } from '@/lib/version';
|
||||||
|
|
||||||
export type HostConsoleCapabilityState = 'loading' | 'allowed' | 'locked';
|
export type HostConsoleCapabilityState = 'loading' | 'allowed' | 'locked';
|
||||||
|
|
||||||
@@ -47,3 +49,42 @@ export function resolveHostConsoleCapability(
|
|||||||
if (!licenseReady) return 'loading';
|
if (!licenseReady) return 'loading';
|
||||||
return isPaid ? 'allowed' : 'locked';
|
return isPaid ? 'allowed' : 'locked';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HostConsoleLockMessageInput {
|
||||||
|
/** Active node mode. Pilot Agent tunnels do not carry Host Console yet. */
|
||||||
|
nodeMode: NodeMode | undefined;
|
||||||
|
nodeName: string;
|
||||||
|
version: string | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lock-card copy for a node whose Host Console capability is missing.
|
||||||
|
*
|
||||||
|
* Pilot Agent nodes cannot advertise the Host Console capability because the
|
||||||
|
* interactive console path is not wired through the Pilot tunnel, so an
|
||||||
|
* upgrade would not enable it. They get transport-specific copy instead of the
|
||||||
|
* generic "upgrade the node" instruction. Every other mode (proxy remote or
|
||||||
|
* missing metadata) keeps the generic upgrade message: version-aware when a
|
||||||
|
* real version is present, otherwise a no-capability hint.
|
||||||
|
*/
|
||||||
|
export function resolveHostConsoleLockMessage(
|
||||||
|
input: HostConsoleLockMessageInput,
|
||||||
|
): { title: string; body: string } {
|
||||||
|
const { nodeMode, nodeName, version } = input;
|
||||||
|
|
||||||
|
if (nodeMode === 'pilot_agent') {
|
||||||
|
return {
|
||||||
|
title: 'Host Console is not available through Pilot Agent yet',
|
||||||
|
body: 'Host Console is currently available on the local node and Distributed API Proxy remotes.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatted = formatVersion(version);
|
||||||
|
const versionHint = formatted
|
||||||
|
? `${nodeName} is running ${formatted}.`
|
||||||
|
: `${nodeName} does not advertise this capability.`;
|
||||||
|
return {
|
||||||
|
title: 'Host Console is not available on this node',
|
||||||
|
body: `${versionHint} Upgrade the node to use this feature.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user