feat(FN-1090): add Capacitor mobile setup for dashboard
- Add Capacitor dependencies and dashboard scripts for sync, open, run, and mobile preparation - Add a typed dashboard capacitor.config.ts with dist/client webDir and FUSION_BACKEND_URL server override - Add Vitest coverage for Capacitor config shape, app identity, webDir, cleartext mode, and env URL behavior - Ignore generated iOS/Android platform directories and document the end-to-end mobile workflow in the dashboard README
This commit is contained in:
@@ -386,6 +386,55 @@ The dashboard includes several runtime safeguards to stay responsive during long
|
||||
- **SSE cleanup and reconnects**: Task and log streaming hooks explicitly clean up EventSource listeners/connections, automatically refetch the task snapshot after a stream reconnect, and avoid duplicate stream setup during rerenders.
|
||||
- **Foreground recovery refresh**: The task board refreshes its task snapshot when the browser tab becomes visible again so long-lived hidden tabs do not keep showing stale board/list data after missed live events.
|
||||
|
||||
## Mobile Development (Capacitor)
|
||||
|
||||
Capacitor wraps the existing Fusion dashboard web build into native iOS and Android shells, so mobile uses the same React UI and API surface as desktop web.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **iOS**: Xcode + CocoaPods installed
|
||||
- **Android**: Android Studio + Android SDK installed
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm --filter @fusion/dashboard mobile:prepare
|
||||
```
|
||||
|
||||
`mobile:prepare` runs `build:client` and then `cap:sync`, copying `dist/client` assets into native platform projects.
|
||||
|
||||
### Running / Opening Native Projects
|
||||
|
||||
```bash
|
||||
pnpm --filter @fusion/dashboard cap:open:ios
|
||||
pnpm --filter @fusion/dashboard cap:open:android
|
||||
```
|
||||
|
||||
You can also run directly:
|
||||
|
||||
```bash
|
||||
pnpm --filter @fusion/dashboard cap:run:ios
|
||||
pnpm --filter @fusion/dashboard cap:run:android
|
||||
```
|
||||
|
||||
### Development with a Live Backend
|
||||
|
||||
Set `FUSION_BACKEND_URL` before sync/run commands so the mobile shell connects to a running Fusion backend (default dashboard backend port is `4040`):
|
||||
|
||||
```bash
|
||||
FUSION_BACKEND_URL=http://YOUR_IP:4040 pnpm --filter @fusion/dashboard cap:sync
|
||||
```
|
||||
|
||||
### Mobile Scripts
|
||||
|
||||
- `cap:sync`
|
||||
- `cap:open:ios`
|
||||
- `cap:open:android`
|
||||
- `cap:run:ios`
|
||||
- `cap:run:android`
|
||||
- `mobile:prepare`
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
|
||||
29
packages/dashboard/capacitor.config.ts
Normal file
29
packages/dashboard/capacitor.config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { CapacitorConfig } from "@capacitor/cli";
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: "com.fusion.dashboard",
|
||||
appName: "Fusion",
|
||||
webDir: "dist/client",
|
||||
server: {
|
||||
// In development, override url to connect to the Fusion backend.
|
||||
// Set FUSION_BACKEND_URL env var before running cap sync/run.
|
||||
// Example: FUSION_BACKEND_URL=http://192.168.1.100:4040 pnpm cap:run:ios
|
||||
//
|
||||
// In production, the app serves static assets from webDir and connects
|
||||
// to the backend at the configured server url.
|
||||
url: process.env.FUSION_BACKEND_URL || undefined,
|
||||
cleartext: true, // Allow HTTP connections to local dev servers
|
||||
},
|
||||
plugins: {
|
||||
SplashScreen: {
|
||||
launchAutoHide: true,
|
||||
backgroundColor: "#0a0a0a",
|
||||
showSpinner: true,
|
||||
spinnerColor: "#6366f1",
|
||||
androidSplashAssetName: "splash",
|
||||
androidScaleType: "CENTER_CROP",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -22,13 +22,20 @@
|
||||
"scripts": {
|
||||
"build": "vite build && tsc",
|
||||
"build:client": "vite build",
|
||||
"cap:open:android": "cap open android",
|
||||
"cap:open:ios": "cap open ios",
|
||||
"cap:run:android": "cap run android",
|
||||
"cap:run:ios": "cap run ios",
|
||||
"cap:sync": "cap sync",
|
||||
"dev": "pnpm build && pnpm typecheck && pnpm dev:serve",
|
||||
"dev:serve": "vite dev",
|
||||
"mobile:prepare": "pnpm build:client && pnpm cap:sync",
|
||||
"postinstall": "chmod +x node_modules/.pnpm/node-pty*/node_modules/node-pty/prebuilds/darwin-*/spawn-helper node_modules/.pnpm/node-pty*/node_modules/node-pty/prebuilds/darwin-*/*.node 2>/dev/null || true",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.app.json",
|
||||
"postinstall": "chmod +x node_modules/.pnpm/node-pty*/node_modules/node-pty/prebuilds/darwin-*/spawn-helper node_modules/.pnpm/node-pty*/node_modules/node-pty/prebuilds/darwin-*/*.node 2>/dev/null || true"
|
||||
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.app.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor/core": "^7.0.0",
|
||||
"@codemirror/basic-setup": "^0.20.0",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-javascript": "^6.2.3",
|
||||
@@ -58,6 +65,9 @@
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/android": "^7.0.0",
|
||||
"@capacitor/cli": "^7.0.0",
|
||||
"@capacitor/ios": "^7.0.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.5.0",
|
||||
|
||||
60
packages/dashboard/src/__tests__/capacitor-config.test.ts
Normal file
60
packages/dashboard/src/__tests__/capacitor-config.test.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import type { CapacitorConfig } from "@capacitor/cli";
|
||||
|
||||
const configPath = resolve(__dirname, "../../capacitor.config.ts");
|
||||
|
||||
function evaluateConfig(env: NodeJS.ProcessEnv = {}): CapacitorConfig {
|
||||
const content = readFileSync(configPath, "utf8");
|
||||
const executableSource = content
|
||||
.replace('import type { CapacitorConfig } from "@capacitor/cli";\n\n', "")
|
||||
.replace("const config: CapacitorConfig = {", "const config = {")
|
||||
.replace("export default config;", "return config;");
|
||||
|
||||
const fn = new Function("process", executableSource) as (processLike: { env: NodeJS.ProcessEnv }) => CapacitorConfig;
|
||||
return fn({ env });
|
||||
}
|
||||
|
||||
describe("capacitor.config", () => {
|
||||
|
||||
it("exists and exports a TypeScript Capacitor config", () => {
|
||||
const content = readFileSync(configPath, "utf8");
|
||||
|
||||
expect(content.length).toBeGreaterThan(0);
|
||||
expect(content).toContain("import type { CapacitorConfig } from \"@capacitor/cli\"");
|
||||
expect(content).toContain("const config: CapacitorConfig = {");
|
||||
expect(content).toContain("export default config;");
|
||||
});
|
||||
|
||||
it("sets webDir to dist/client", () => {
|
||||
const config = evaluateConfig();
|
||||
expect(config.webDir).toBe("dist/client");
|
||||
});
|
||||
|
||||
it("uses the expected app identity", () => {
|
||||
const config = evaluateConfig();
|
||||
expect(config.appId).toBe("com.fusion.dashboard");
|
||||
expect(config.appName).toBe("Fusion");
|
||||
});
|
||||
|
||||
it("enables cleartext server access for local development", () => {
|
||||
const config = evaluateConfig();
|
||||
expect(config.server?.cleartext).toBe(true);
|
||||
});
|
||||
|
||||
it("defaults server.url to undefined when FUSION_BACKEND_URL is unset", () => {
|
||||
const config = evaluateConfig({});
|
||||
expect(config.server?.url).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses FUSION_BACKEND_URL when provided", () => {
|
||||
const config = evaluateConfig({ FUSION_BACKEND_URL: "http://192.168.1.100:4040" });
|
||||
expect(config.server?.url).toBe("http://192.168.1.100:4040");
|
||||
});
|
||||
|
||||
it("references the FUSION_BACKEND_URL env variable in source", () => {
|
||||
const content = readFileSync(configPath, "utf8");
|
||||
expect(content).toContain("process.env.FUSION_BACKEND_URL || undefined");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user