feat(FN-3406): add shell context hook plumbing for dashboard
Adds a new `useShellContext` hook with tests to the dashboard, integrated into `App.tsx` and `Header.tsx` to provide shell context plumbing throughout the UI. Fusion-Task-Id: FN-3406
This commit is contained in:
64
packages/mobile/src/__tests__/shell-handoff.test.ts
Normal file
64
packages/mobile/src/__tests__/shell-handoff.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildMobileShellHandoff } from "../plugins/shell-handoff.js";
|
||||
|
||||
describe("buildMobileShellHandoff", () => {
|
||||
it("builds remote handoff URL for valid active profile", () => {
|
||||
const result = buildMobileShellHandoff({
|
||||
host: "mobile-shell",
|
||||
activeProfileId: "profile_1",
|
||||
profiles: [
|
||||
{
|
||||
id: "profile_1",
|
||||
name: "Prod",
|
||||
serverUrl: "https://fusion.example.com/",
|
||||
authToken: "abc123",
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
lastUsedAt: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.kind).toBe("remote-launch");
|
||||
if (result.kind !== "remote-launch") {
|
||||
throw new Error("expected remote-launch");
|
||||
}
|
||||
|
||||
const url = new URL(result.url);
|
||||
expect(url.searchParams.get("shellKind")).toBe("mobile");
|
||||
expect(url.searchParams.get("shellMode")).toBe("remote");
|
||||
expect(url.searchParams.get("profileId")).toBe("profile_1");
|
||||
expect(url.searchParams.get("serverBaseUrl")).toBe("https://fusion.example.com");
|
||||
expect(url.searchParams.get("token")).toBe("abc123");
|
||||
});
|
||||
|
||||
it("returns deterministic fallback when no active profile exists", () => {
|
||||
const result = buildMobileShellHandoff({
|
||||
host: "mobile-shell",
|
||||
activeProfileId: null,
|
||||
profiles: [],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ kind: "fallback", reason: "no-active-profile" });
|
||||
});
|
||||
|
||||
it("returns fallback for invalid server URLs", () => {
|
||||
const result = buildMobileShellHandoff({
|
||||
host: "mobile-shell",
|
||||
activeProfileId: "profile_1",
|
||||
profiles: [
|
||||
{
|
||||
id: "profile_1",
|
||||
name: "Prod",
|
||||
serverUrl: "not-a-url",
|
||||
authToken: null,
|
||||
createdAt: "",
|
||||
updatedAt: "",
|
||||
lastUsedAt: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result).toEqual({ kind: "fallback", reason: "invalid-server-url" });
|
||||
});
|
||||
});
|
||||
@@ -22,6 +22,7 @@ export type {
|
||||
} from "./plugins/push-notifications.js";
|
||||
export { ShareManager } from "./plugins/share.js";
|
||||
export { MobileNativeShellBridge } from "./plugins/native-shell.js";
|
||||
export { buildMobileShellHandoff } from "./plugins/shell-handoff.js";
|
||||
export { QrScanner, parseQrConnectionPayload } from "./plugins/qr-scanner.js";
|
||||
export {
|
||||
loadShellProfiles,
|
||||
@@ -42,6 +43,8 @@ export type {
|
||||
ShellConnectionProfile,
|
||||
ShellConnectionProfileInput,
|
||||
ShellConnectionState,
|
||||
MobileShellHandoffResult,
|
||||
MobileRemoteShellLaunch,
|
||||
} from "./types.js";
|
||||
|
||||
interface LifecycleManager {
|
||||
|
||||
74
packages/mobile/src/plugins/shell-handoff.ts
Normal file
74
packages/mobile/src/plugins/shell-handoff.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type {
|
||||
MobileRemoteShellLaunch,
|
||||
ShellConnectionProfile,
|
||||
ShellConnectionState,
|
||||
MobileShellHandoffResult,
|
||||
} from "../types.js";
|
||||
|
||||
export const SHELL_HANDOFF_QUERY = {
|
||||
shellKind: "shellKind",
|
||||
shellMode: "shellMode",
|
||||
profileId: "profileId",
|
||||
serverBaseUrl: "serverBaseUrl",
|
||||
serverLabel: "serverLabel",
|
||||
token: "token",
|
||||
canOpenConnectionManager: "shellCanOpenConnectionManager",
|
||||
} as const;
|
||||
|
||||
function normalizeServerBaseUrl(input: string): string | null {
|
||||
try {
|
||||
const parsed = new URL(input);
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
return null;
|
||||
}
|
||||
return parsed.toString().replace(/\/$/, "");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function findActiveProfile(state: ShellConnectionState): ShellConnectionProfile | null {
|
||||
if (!state.activeProfileId) {
|
||||
return null;
|
||||
}
|
||||
return state.profiles.find((profile) => profile.id === state.activeProfileId) ?? null;
|
||||
}
|
||||
|
||||
export function buildMobileShellHandoff(state: ShellConnectionState): MobileShellHandoffResult {
|
||||
const activeProfile = findActiveProfile(state);
|
||||
if (!state.activeProfileId) {
|
||||
return { kind: "fallback", reason: "no-active-profile" };
|
||||
}
|
||||
if (!activeProfile) {
|
||||
return { kind: "fallback", reason: "missing-profile" };
|
||||
}
|
||||
|
||||
const serverBaseUrl = normalizeServerBaseUrl(activeProfile.serverUrl);
|
||||
if (!serverBaseUrl) {
|
||||
return { kind: "fallback", reason: "invalid-server-url" };
|
||||
}
|
||||
|
||||
const launch: MobileRemoteShellLaunch = {
|
||||
shellKind: "mobile",
|
||||
shellMode: "remote",
|
||||
profileId: activeProfile.id,
|
||||
serverBaseUrl,
|
||||
...(activeProfile.name ? { serverLabel: activeProfile.name } : {}),
|
||||
...(activeProfile.authToken ? { token: activeProfile.authToken } : {}),
|
||||
};
|
||||
|
||||
const url = new URL(serverBaseUrl);
|
||||
url.searchParams.set(SHELL_HANDOFF_QUERY.shellKind, launch.shellKind);
|
||||
url.searchParams.set(SHELL_HANDOFF_QUERY.shellMode, launch.shellMode);
|
||||
url.searchParams.set(SHELL_HANDOFF_QUERY.profileId, launch.profileId);
|
||||
url.searchParams.set(SHELL_HANDOFF_QUERY.serverBaseUrl, launch.serverBaseUrl);
|
||||
if (launch.serverLabel) {
|
||||
url.searchParams.set(SHELL_HANDOFF_QUERY.serverLabel, launch.serverLabel);
|
||||
}
|
||||
if (launch.token) {
|
||||
url.searchParams.set(SHELL_HANDOFF_QUERY.token, launch.token);
|
||||
}
|
||||
url.searchParams.set(SHELL_HANDOFF_QUERY.canOpenConnectionManager, "1");
|
||||
|
||||
return { kind: "remote-launch", launch, url: url.toString() };
|
||||
}
|
||||
@@ -38,6 +38,19 @@ export interface ShellConnectionState {
|
||||
};
|
||||
}
|
||||
|
||||
export interface MobileRemoteShellLaunch {
|
||||
shellKind: "mobile";
|
||||
shellMode: "remote";
|
||||
profileId: string;
|
||||
serverBaseUrl: string;
|
||||
serverLabel?: string;
|
||||
token?: string;
|
||||
}
|
||||
|
||||
export type MobileShellHandoffResult =
|
||||
| { kind: "remote-launch"; url: string; launch: MobileRemoteShellLaunch }
|
||||
| { kind: "fallback"; reason: "no-active-profile" | "missing-profile" | "invalid-server-url" };
|
||||
|
||||
export interface FusionShellApi {
|
||||
getState(): Promise<ShellConnectionState>;
|
||||
listProfiles(): Promise<ShellConnectionProfile[]>;
|
||||
|
||||
Reference in New Issue
Block a user