feat(FN-3577): add native shell guide and bridge contract documentation
Adds a native shell integration guide (FN-3577) with fusionShell bridge contract documentation, wired into the docs navigation, plus a regression test; also hardens the dependency graph's task filtering (FN-3085) to include in-review tasks and handle orphan edges, with corresponding test coverage ac Fusion-Task-Id: FN-3577
This commit is contained in:
11
MOBILE.md
11
MOBILE.md
@@ -79,10 +79,13 @@ Install from browser:
|
|||||||
|
|
||||||
### Native shell onboarding and connection profiles
|
### Native shell onboarding and connection profiles
|
||||||
|
|
||||||
- First launch in the mobile shell enters a shell-level remote connection onboarding flow before dashboard model onboarding.
|
First launch in the mobile shell enters a shell-level remote connection onboarding flow before dashboard model onboarding.
|
||||||
- Connection setup supports QR scan and manual URL entry, with optional auth token.
|
|
||||||
- Saved remote profiles are persisted in shell-local mobile storage and managed through `window.fusionShell` connection APIs.
|
For the canonical flow (QR/manual setup, saved profiles, active-profile behavior, and security caveats), see [Native Shell Connection Guide](./docs/native-shell.md).
|
||||||
- Shell connection persistence is intentionally separate from Fusion project/global settings.
|
|
||||||
|
Implementation notes:
|
||||||
|
- Mobile shell profiles are persisted in shell-local storage (Capacitor Preferences), separate from Fusion project/global settings.
|
||||||
|
- The dashboard consumes this through the shared `window.fusionShell` connection APIs.
|
||||||
|
|
||||||
### Planning Mode
|
### Planning Mode
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ For a full walkthrough (installation, onboarding, first task, and daily workflow
|
|||||||
| [Dashboard Guide](./dashboard-guide.md) | Board/list views, terminal, git manager, files, planning, and UI tools |
|
| [Dashboard Guide](./dashboard-guide.md) | Board/list views, terminal, git manager, files, planning, and UI tools |
|
||||||
| [CLI Reference](./cli-reference.md) | Complete `fn` command reference with subcommands, flags, and examples |
|
| [CLI Reference](./cli-reference.md) | Complete `fn` command reference with subcommands, flags, and examples |
|
||||||
| [Remote Access](./remote-access.md) | Operator runbook for Tailscale/Cloudflare setup, tokenized login links, security caveats, and troubleshooting |
|
| [Remote Access](./remote-access.md) | Operator runbook for Tailscale/Cloudflare setup, tokenized login links, security caveats, and troubleshooting |
|
||||||
|
| [Native Shell Connection Guide](./native-shell.md) | Canonical mobile/desktop shell onboarding, profile management, QR/manual setup, and remote handoff behavior |
|
||||||
|
|
||||||
### Task & Project Management
|
### Task & Project Management
|
||||||
| Guide | Description |
|
| Guide | Description |
|
||||||
|
|||||||
@@ -21,6 +21,38 @@ At a high level, Fusion is split into:
|
|||||||
|
|
||||||
Native shells expose a shared host-neutral bridge at `window.fusionShell` for first-run shell onboarding, connection profile persistence, and active shell mode/profile state. The dashboard consumes `window.fusionShell` when present and degrades cleanly in plain web/PWA mode.
|
Native shells expose a shared host-neutral bridge at `window.fusionShell` for first-run shell onboarding, connection profile persistence, and active shell mode/profile state. The dashboard consumes `window.fusionShell` when present and degrades cleanly in plain web/PWA mode.
|
||||||
|
|
||||||
|
### `window.fusionShell` bridge contract
|
||||||
|
|
||||||
|
Canonical dashboard-side types live in `packages/dashboard/app/types/native-shell.d.ts`.
|
||||||
|
|
||||||
|
Shared bridge methods used by dashboard/mobile/desktop flows:
|
||||||
|
- `getState()`
|
||||||
|
- `listProfiles()`
|
||||||
|
- `saveProfile(profile)`
|
||||||
|
- `deleteProfile(profileId)`
|
||||||
|
- `setActiveProfile(profileId)`
|
||||||
|
- `setDesktopMode(mode)`
|
||||||
|
- `startQrScan()`
|
||||||
|
- `openConnectionManager()`
|
||||||
|
- `subscribe(listener)`
|
||||||
|
|
||||||
|
Shared shell state contract (`ShellConnectionState`):
|
||||||
|
- `host` (`"web" | "mobile-shell" | "desktop-shell"`)
|
||||||
|
- `desktopMode` (`"local" | "remote"`, optional)
|
||||||
|
- `activeProfileId`
|
||||||
|
- `profiles`
|
||||||
|
- `localServer` (`status`, optional `port`, optional `error`)
|
||||||
|
|
||||||
|
Desktop-specific bootstrap extension:
|
||||||
|
- Electron preload also exposes `getDesktopModeState()` for first-run desktop mode selection (`{ isFirstRun, desktopMode }`).
|
||||||
|
- The dashboard itself does **not** depend on that preload-only helper for steady-state rendering; it consumes shared shell state via `ShellContext` (`packages/dashboard/app/context/ShellContext.tsx`).
|
||||||
|
|
||||||
|
Persistence ownership by host:
|
||||||
|
- **Mobile shell** persists connection profiles + active profile with Capacitor Preferences (`packages/mobile/src/plugins/connection-profiles.ts`).
|
||||||
|
- **Desktop shell** persists shell settings in app-owned JSON at `app.getPath("userData")/shell-connections.json` (`packages/desktop/src/shell-settings.ts`).
|
||||||
|
|
||||||
|
These are shell-owned persistence layers, intentionally separate from Fusion project/global settings.
|
||||||
|
|
||||||
### High-level runtime diagram
|
### High-level runtime diagram
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -379,6 +379,12 @@ Click the chevron next to the status indicator to open the node selector dropdow
|
|||||||
|
|
||||||
The selected node persists across browser sessions via localStorage. If the selected remote node is unregistered, the dashboard automatically falls back to local mode.
|
The selected node persists across browser sessions via localStorage. If the selected remote node is unregistered, the dashboard automatically falls back to local mode.
|
||||||
|
|
||||||
|
## Native shell connection flow
|
||||||
|
|
||||||
|
If you use Fusion from a native shell (mobile app or desktop shell in remote mode), dashboard startup is gated by shell onboarding until a connection is selected.
|
||||||
|
|
||||||
|
For the canonical workflow (first-run onboarding, QR/manual setup, saved profiles, and desktop local/remote handoff), see [Native Shell Connection Guide](./native-shell.md).
|
||||||
|
|
||||||
## Remote Access (Settings)
|
## Remote Access (Settings)
|
||||||
|
|
||||||
Dashboard remote controls live in **Settings → Remote Access**.
|
Dashboard remote controls live in **Settings → Remote Access**.
|
||||||
|
|||||||
95
docs/native-shell.md
Normal file
95
docs/native-shell.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# Native Shell Connection Guide
|
||||||
|
|
||||||
|
[← Docs index](./README.md)
|
||||||
|
|
||||||
|
This is the canonical guide for how Fusion native shells (mobile and desktop) connect to remote Fusion dashboards, persist saved connections, and hand off shell state to the dashboard.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Native shells expose a shared `window.fusionShell` bridge that the dashboard reads through `ShellContext`.
|
||||||
|
|
||||||
|
- **Mobile shell** always runs in remote mode and requires an active connection profile.
|
||||||
|
- **Desktop shell** supports both **Local Fusion** and **Remote Server** modes.
|
||||||
|
- **Web/PWA** does not use shell onboarding.
|
||||||
|
|
||||||
|
## First-run onboarding flow
|
||||||
|
|
||||||
|
The dashboard gates first-run shell onboarding in `requiresNativeShellOnboarding(...)` (`packages/dashboard/app/App.tsx`):
|
||||||
|
|
||||||
|
- `host === "mobile-shell"`: onboarding is required until `activeProfileId` is set.
|
||||||
|
- `host === "desktop-shell"`:
|
||||||
|
- `desktopMode === "local"`: onboarding is not required.
|
||||||
|
- `desktopMode === "remote"` (or unset): onboarding is required until `activeProfileId` is set.
|
||||||
|
|
||||||
|
`NativeShellOnboardingModal` then provides:
|
||||||
|
|
||||||
|
1. **Desktop mode choice** (desktop only):
|
||||||
|
- **Local Fusion** → calls `setDesktopMode("local")`.
|
||||||
|
- **Remote Server** → stays in remote flow.
|
||||||
|
2. **Remote connection entry** (mobile + desktop remote mode):
|
||||||
|
- **Scan QR** (`startQrScan()`)
|
||||||
|
- Manual fields: profile name, server URL, optional auth token
|
||||||
|
3. **Continue**:
|
||||||
|
- Saves profile via `saveProfile(...)`
|
||||||
|
- Sets desktop mode to remote when relevant
|
||||||
|
- Activates profile via `setActiveProfile(...)`
|
||||||
|
- Redirects to selected remote dashboard URL (adds `rt=<token>` query when token is present)
|
||||||
|
|
||||||
|
## QR scan and manual fallback
|
||||||
|
|
||||||
|
QR scan is optional convenience. Manual entry is always supported.
|
||||||
|
|
||||||
|
QR payload parsing accepts either:
|
||||||
|
|
||||||
|
- JSON payload with `serverUrl` and optional `authToken`
|
||||||
|
- URL payload, reading token from `authToken` or `rt`
|
||||||
|
|
||||||
|
If scanning fails or is unavailable, users can continue with manual URL + token entry.
|
||||||
|
|
||||||
|
## Saved connection profiles
|
||||||
|
|
||||||
|
Profiles are first-class saved objects shared by onboarding and Connection Manager:
|
||||||
|
|
||||||
|
- `name`
|
||||||
|
- `serverUrl`
|
||||||
|
- optional `authToken`
|
||||||
|
- timestamps (`createdAt`, `updatedAt`, `lastUsedAt`)
|
||||||
|
|
||||||
|
Connection Manager supports:
|
||||||
|
|
||||||
|
- **Use** (activate profile)
|
||||||
|
- **Edit** (update name/URL/token)
|
||||||
|
- **Delete**
|
||||||
|
- **Add connection**
|
||||||
|
|
||||||
|
Activation updates `activeProfileId` and stamps `lastUsedAt` on the selected profile.
|
||||||
|
|
||||||
|
## Desktop remote handoff behavior
|
||||||
|
|
||||||
|
Desktop shell stores shell settings separately from Fusion project/global settings.
|
||||||
|
|
||||||
|
When desktop mode is `remote` and an active profile exists, `App.tsx` redirects to that profile URL and appends `rt` when a token exists.
|
||||||
|
|
||||||
|
When desktop mode is `local` and the local server reports `ready` with a port, `App.tsx` redirects to `http://localhost:<port>`.
|
||||||
|
|
||||||
|
## Persistence model
|
||||||
|
|
||||||
|
- **Mobile shell**: connection profiles + active profile are persisted through Capacitor Preferences (`packages/mobile/src/plugins/connection-profiles.ts`).
|
||||||
|
- **Desktop shell**: shell settings are persisted in app-owned JSON at `app.getPath("userData")/shell-connections.json` (`packages/desktop/src/shell-settings.ts`).
|
||||||
|
|
||||||
|
## Security guidance
|
||||||
|
|
||||||
|
Tokenized URLs and QR payloads are secrets.
|
||||||
|
|
||||||
|
- Treat `rt`/`authToken` values like passwords.
|
||||||
|
- Do not paste tokenized links into chats, screenshots, or tickets.
|
||||||
|
- Prefer short-lived token workflows when sharing access.
|
||||||
|
|
||||||
|
For canonical token caveats and operator guidance, see [Remote Access runbook](./remote-access.md).
|
||||||
|
|
||||||
|
## Related docs
|
||||||
|
|
||||||
|
- [Dashboard Guide](./dashboard-guide.md)
|
||||||
|
- [Architecture](./architecture.md)
|
||||||
|
- [Mobile Development Guide](../MOBILE.md)
|
||||||
|
- [Remote Access runbook](./remote-access.md)
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
// @vitest-environment node
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
const repoRoot = path.resolve(__dirname, "../../../../");
|
||||||
|
|
||||||
|
function readDoc(relativePath: string): string {
|
||||||
|
return readFileSync(path.join(repoRoot, relativePath), "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("native shell documentation contract", () => {
|
||||||
|
it("publishes the canonical native shell guide with required flow sections", () => {
|
||||||
|
const nativeShellGuide = readDoc("docs/native-shell.md");
|
||||||
|
|
||||||
|
expect(nativeShellGuide).toContain("# Native Shell Connection Guide");
|
||||||
|
expect(nativeShellGuide).toContain("## First-run onboarding flow");
|
||||||
|
expect(nativeShellGuide).toContain("## QR scan and manual fallback");
|
||||||
|
expect(nativeShellGuide).toContain("## Saved connection profiles");
|
||||||
|
expect(nativeShellGuide).toContain("## Desktop remote handoff behavior");
|
||||||
|
expect(nativeShellGuide).toContain("window.fusionShell");
|
||||||
|
expect(nativeShellGuide).toContain("Remote Access runbook");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps cross-doc links to the canonical guide and bridge API discoverable", () => {
|
||||||
|
const docsIndex = readDoc("docs/README.md");
|
||||||
|
const dashboardGuide = readDoc("docs/dashboard-guide.md");
|
||||||
|
const mobileGuide = readDoc("MOBILE.md");
|
||||||
|
const architecture = readDoc("docs/architecture.md");
|
||||||
|
|
||||||
|
expect(docsIndex).toContain("[Native Shell Connection Guide](./native-shell.md)");
|
||||||
|
expect(dashboardGuide).toContain("[Native Shell Connection Guide](./native-shell.md)");
|
||||||
|
expect(mobileGuide).toContain("[Native Shell Connection Guide](./docs/native-shell.md)");
|
||||||
|
|
||||||
|
expect(architecture).toContain("window.fusionShell");
|
||||||
|
expect(architecture).toContain("getState()");
|
||||||
|
expect(architecture).toContain("listProfiles()");
|
||||||
|
expect(architecture).toContain("saveProfile(profile)");
|
||||||
|
expect(architecture).toContain("deleteProfile(profileId)");
|
||||||
|
expect(architecture).toContain("setActiveProfile(profileId)");
|
||||||
|
expect(architecture).toContain("setDesktopMode(mode)");
|
||||||
|
expect(architecture).toContain("startQrScan()");
|
||||||
|
expect(architecture).toContain("openConnectionManager()");
|
||||||
|
expect(architecture).toContain("subscribe(listener)");
|
||||||
|
expect(architecture).toContain("getDesktopModeState()");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -528,14 +528,18 @@ const { totalWorkers, concurrency } = defaultTestWorkerBudget(process.env);
|
|||||||
|
|
||||||
const isolatedHomesToCleanup = new Set();
|
const isolatedHomesToCleanup = new Set();
|
||||||
|
|
||||||
|
function cleanupIsolatedHomePath(homePath) {
|
||||||
|
try {
|
||||||
|
rmSync(homePath, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// Best-effort cleanup only.
|
||||||
|
}
|
||||||
|
isolatedHomesToCleanup.delete(homePath);
|
||||||
|
}
|
||||||
|
|
||||||
function cleanupIsolatedHomes() {
|
function cleanupIsolatedHomes() {
|
||||||
for (const homePath of isolatedHomesToCleanup) {
|
for (const homePath of isolatedHomesToCleanup) {
|
||||||
try {
|
cleanupIsolatedHomePath(homePath);
|
||||||
rmSync(homePath, { recursive: true, force: true });
|
|
||||||
} catch {
|
|
||||||
// Best-effort cleanup only.
|
|
||||||
}
|
|
||||||
isolatedHomesToCleanup.delete(homePath);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -552,6 +556,7 @@ process.on("SIGTERM", () => {
|
|||||||
export function createIsolatedHomeEnv(env = process.env) {
|
export function createIsolatedHomeEnv(env = process.env) {
|
||||||
const rawIsolatedHome = mkdtempSync(path.join(tmpdir(), "fusion-test-home-root-"));
|
const rawIsolatedHome = mkdtempSync(path.join(tmpdir(), "fusion-test-home-root-"));
|
||||||
const isolatedHome = realpathSync(rawIsolatedHome);
|
const isolatedHome = realpathSync(rawIsolatedHome);
|
||||||
|
isolatedHomesToCleanup.add(rawIsolatedHome);
|
||||||
isolatedHomesToCleanup.add(isolatedHome);
|
isolatedHomesToCleanup.add(isolatedHome);
|
||||||
|
|
||||||
const nextEnv = {
|
const nextEnv = {
|
||||||
@@ -613,8 +618,7 @@ export function main(argv = process.argv.slice(2)) {
|
|||||||
const { env: isolatedHomeEnv, isolatedHome } = createIsolatedHomeEnv(fullSuiteEnv);
|
const { env: isolatedHomeEnv, isolatedHome } = createIsolatedHomeEnv(fullSuiteEnv);
|
||||||
|
|
||||||
const cleanupIsolatedHome = () => {
|
const cleanupIsolatedHome = () => {
|
||||||
rmSync(isolatedHome, { recursive: true, force: true });
|
cleanupIsolatedHomePath(isolatedHome);
|
||||||
isolatedHomesToCleanup.delete(isolatedHome);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user