Compare commits
4 Commits
feat/ms23-
...
feat/ms23-
| Author | SHA1 | Date | |
|---|---|---|---|
| 487aac6903 | |||
| 544e828e58 | |||
| 9489bc63f8 | |||
| ad644799aa |
@@ -0,0 +1,67 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import type { AgentSession } from "@mosaic/shared";
|
||||||
|
import type { PrismaService } from "../../prisma/prisma.service";
|
||||||
|
import { AgentProviderRegistry } from "../agents/agent-provider.registry";
|
||||||
|
import { MissionControlController } from "./mission-control.controller";
|
||||||
|
import { MissionControlService } from "./mission-control.service";
|
||||||
|
|
||||||
|
describe("MissionControlController", () => {
|
||||||
|
let controller: MissionControlController;
|
||||||
|
let registry: {
|
||||||
|
listAllSessions: ReturnType<typeof vi.fn>;
|
||||||
|
getProviderForSession: ReturnType<typeof vi.fn>;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
registry = {
|
||||||
|
listAllSessions: vi.fn(),
|
||||||
|
getProviderForSession: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const prisma = {
|
||||||
|
operatorAuditLog: {
|
||||||
|
create: vi.fn().mockResolvedValue(undefined),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = new MissionControlService(
|
||||||
|
registry as unknown as AgentProviderRegistry,
|
||||||
|
prisma as unknown as PrismaService
|
||||||
|
);
|
||||||
|
|
||||||
|
controller = new MissionControlController(service);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Phase 1 gate: unified sessions endpoint returns internal provider sessions", async () => {
|
||||||
|
const internalSession: AgentSession = {
|
||||||
|
id: "session-internal-1",
|
||||||
|
providerId: "internal",
|
||||||
|
providerType: "internal",
|
||||||
|
status: "active",
|
||||||
|
createdAt: new Date("2026-03-07T20:00:00.000Z"),
|
||||||
|
updatedAt: new Date("2026-03-07T20:01:00.000Z"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const externalSession: AgentSession = {
|
||||||
|
id: "session-openclaw-1",
|
||||||
|
providerId: "openclaw",
|
||||||
|
providerType: "external",
|
||||||
|
status: "active",
|
||||||
|
createdAt: new Date("2026-03-07T20:02:00.000Z"),
|
||||||
|
updatedAt: new Date("2026-03-07T20:03:00.000Z"),
|
||||||
|
};
|
||||||
|
|
||||||
|
registry.listAllSessions.mockResolvedValue([internalSession, externalSession]);
|
||||||
|
|
||||||
|
const response = await controller.listSessions();
|
||||||
|
|
||||||
|
expect(registry.listAllSessions).toHaveBeenCalledTimes(1);
|
||||||
|
expect(response.sessions).toEqual([internalSession, externalSession]);
|
||||||
|
expect(response.sessions).toContainEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: "session-internal-1",
|
||||||
|
providerId: "internal",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { MissionControlLayout } from "@/components/mission-control/MissionControlLayout";
|
||||||
|
|
||||||
|
export default function MissionControlPage(): React.JSX.Element {
|
||||||
|
return <MissionControlLayout />;
|
||||||
|
}
|
||||||
@@ -156,6 +156,26 @@ function IconTerminal(): React.JSX.Element {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function IconMissionControl(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle cx="8" cy="8" r="1.5" />
|
||||||
|
<path d="M11 5a4.25 4.25 0 0 1 0 6" />
|
||||||
|
<path d="M5 5a4.25 4.25 0 0 0 0 6" />
|
||||||
|
<path d="M13.5 2.5a7.75 7.75 0 0 1 0 11" />
|
||||||
|
<path d="M2.5 2.5a7.75 7.75 0 0 0 0 11" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function IconSettings(): React.JSX.Element {
|
function IconSettings(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@@ -260,6 +280,11 @@ const NAV_GROUPS: NavGroup[] = [
|
|||||||
label: "Terminal",
|
label: "Terminal",
|
||||||
icon: <IconTerminal />,
|
icon: <IconTerminal />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: "/mission-control",
|
||||||
|
label: "Mission Control",
|
||||||
|
icon: <IconMissionControl />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
|
export function GlobalAgentRoster(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<Card className="flex h-full min-h-0 flex-col">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Agent Roster</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
|
||||||
|
No active agents
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { GlobalAgentRoster } from "@/components/mission-control/GlobalAgentRoster";
|
||||||
|
import { MissionControlPanel } from "@/components/mission-control/MissionControlPanel";
|
||||||
|
|
||||||
|
const DEFAULT_PANEL_SLOTS = ["panel-1", "panel-2", "panel-3", "panel-4"] as const;
|
||||||
|
|
||||||
|
export function MissionControlLayout(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<section className="h-full min-h-0 overflow-hidden" aria-label="Mission Control">
|
||||||
|
<div className="grid h-full min-h-0 gap-4 xl:grid-cols-[280px_minmax(0,1fr)]">
|
||||||
|
<aside className="h-full min-h-0">
|
||||||
|
<GlobalAgentRoster />
|
||||||
|
</aside>
|
||||||
|
<main className="h-full min-h-0 overflow-hidden">
|
||||||
|
<MissionControlPanel panels={DEFAULT_PANEL_SLOTS} />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { OrchestratorPanel } from "@/components/mission-control/OrchestratorPanel";
|
||||||
|
|
||||||
|
interface MissionControlPanelProps {
|
||||||
|
panels: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MissionControlPanel({ panels }: MissionControlPanelProps): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="grid h-full min-h-0 auto-rows-fr grid-cols-1 gap-4 overflow-y-auto pr-1 md:grid-cols-2">
|
||||||
|
{panels.map((panelId) => (
|
||||||
|
<OrchestratorPanel key={panelId} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
|
export function OrchestratorPanel(): React.JSX.Element {
|
||||||
|
return (
|
||||||
|
<Card className="flex h-full min-h-[220px] flex-col">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Orchestrator Panel</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-1 items-center justify-center text-sm text-muted-foreground">
|
||||||
|
Select an agent
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
10
apps/web/src/hooks/useMissionControl.ts
Normal file
10
apps/web/src/hooks/useMissionControl.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
interface UseMissionControlResult {
|
||||||
|
sessions: [];
|
||||||
|
loading: boolean;
|
||||||
|
error: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stub — will be wired in P2-002
|
||||||
|
export function useMissionControl(): UseMissionControlResult {
|
||||||
|
return { sessions: [], loading: false, error: null };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user