feat(host-console): gate Host Console behind Admiral tier (#277)

* feat(host-console): gate Host Console behind Admiral tier

Move the Host Console from the Community (free) tier to Admiral,
enforcing the gate at every layer: UI nav visibility, AdmiralGate
wrapper, POST /api/system/console-token endpoint, and the WebSocket
upgrade handler for /api/system/host-console.

* test(auth): mock Admiral license for console-token test

The console-token endpoint now requires Admiral tier. Mock
LicenseService in the test to return pro/team so the happy-path
test passes in CI where no license is activated.
This commit is contained in:
Anso
2026-03-30 15:56:58 -04:00
committed by GitHub
parent ccf6062473
commit b5d3f497cb
7 changed files with 49 additions and 6 deletions
+5 -2
View File
@@ -7,6 +7,7 @@ import ErrorBoundary from './ErrorBoundary';
import HomeDashboard from './HomeDashboard';
import BashExecModal from './BashExecModal';
import HostConsole from './HostConsole';
import { AdmiralGate } from './AdmiralGate';
import ResourcesView from './ResourcesView';
import { Button } from './ui/button';
import { Input } from './ui/input';
@@ -161,13 +162,13 @@ export default function EditorLayout() {
{ value: 'dashboard', label: 'Home', icon: Home },
{ value: 'fleet', label: 'Fleet', icon: Radar },
];
if (isAdmin) items.push({ value: 'host-console', label: 'Console', icon: Terminal });
items.push(
{ value: 'resources', label: 'Resources', icon: HardDrive },
{ value: 'templates', label: 'App Store', icon: CloudDownload },
{ value: 'global-observability', label: 'Logs', icon: Activity },
);
if (isPro && license?.variant === 'team') {
if (isAdmin) items.push({ value: 'host-console', label: 'Console', icon: Terminal });
if (can('system:audit')) items.push({ value: 'audit-log', label: 'Audit', icon: ScrollText });
if (isAdmin) items.push({ value: 'scheduled-ops', label: 'Schedules', icon: Clock });
}
@@ -1479,7 +1480,9 @@ export default function EditorLayout() {
) : activeView === 'resources' ? (
<ResourcesView />
) : activeView === 'host-console' ? (
<HostConsole stackName={selectedFile} onClose={() => setActiveView(selectedFile ? 'editor' : 'dashboard')} />
<AdmiralGate featureName="Host Console">
<HostConsole stackName={selectedFile} onClose={() => setActiveView(selectedFile ? 'editor' : 'dashboard')} />
</AdmiralGate>
) : !isLoading && selectedFile && activeView === 'editor' ? (
<ErrorBoundary>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 items-stretch">