Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ Thumbs.db

# Video production belongs in the separate private repository.
/videos/

# Mobile generated SDK projects and build outputs
ui/dist-mobile/
mobile/src-tauri/gen/
!/mobile/src-tauri/Cargo.lock
mobile/src-tauri/.tauri/
93 changes: 93 additions & 0 deletions docs/evidence/mobile-tauri-20261019/android-build.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@

> ferryx-mobile@0.1.0 android:build
> node scripts/tauri.mjs android build --debug --target aarch64 --apk

Info Using installed NDK: <home>/android-sdk/ndk/27.2.12479018
Info Looking up installed tauri packages to check mismatched versions...
Running beforeBuildCommand `npm --prefix ../ui run build:mobile`

> ui@0.1.0 build:mobile
> tsc && vite build --config vite.mobile.config.ts

vite v6.4.4 building for production...
transforming...
✓ 2258 modules transformed.
rendering chunks...
computing gzip size...
dist-mobile/index.html 3.39 kB │ gzip: 1.22 kB
dist-mobile/assets/crush-DHElXcRZ.png 5.46 kB
dist-mobile/assets/droid-BhrKLgQ8.svg 6.30 kB │ gzip: 2.88 kB
dist-mobile/assets/antigravity-DgAQAdn3.svg 7.63 kB │ gzip: 1.72 kB
dist-mobile/assets/gjc-CmlXGg4z.png 22.73 kB
dist-mobile/assets/geist-variable-CrgPqtmy.woff2 69.44 kB
dist-mobile/assets/ferryx-icon-OXRkkUvz.png 805.82 kB
dist-mobile/assets/App-V8Wj_Bb0.css 0.59 kB │ gzip: 0.31 kB
dist-mobile/assets/index-ckLza-72.css 113.95 kB │ gzip: 20.02 kB
dist-mobile/assets/arrow-down-Ev2p6eA6.js 0.34 kB │ gzip: 0.27 kB
dist-mobile/assets/nativeShortcutTable-ChDHIVvA.js 0.57 kB │ gzip: 0.35 kB
dist-mobile/assets/arrow-right-CACxJEnM.js 0.62 kB │ gzip: 0.32 kB
dist-mobile/assets/clipboard-CCgcnRg2.js 1.69 kB │ gzip: 0.83 kB
dist-mobile/assets/shortcutOverrides-CEXeEYvN.js 1.81 kB │ gzip: 0.85 kB
dist-mobile/assets/tailscaleDiscovery-BtfcwLaD.js 2.56 kB │ gzip: 1.11 kB
dist-mobile/assets/WhatsNewDialog-BTJXmGhU.js 2.85 kB │ gzip: 1.16 kB
dist-mobile/assets/sshHosts-CWzxb_af.js 3.22 kB │ gzip: 1.29 kB
dist-mobile/assets/SystemResourcesDialog-Dx4piHvl.js 7.05 kB │ gzip: 2.62 kB
dist-mobile/assets/AccountSignIn-CbolCq8y.js 7.16 kB │ gzip: 2.70 kB
dist-mobile/assets/pairedHostInventory-DJG7kEq_.js 14.65 kB │ gzip: 5.03 kB
dist-mobile/assets/browserProtocol-Ck8ZGqNi.js 16.42 kB │ gzip: 4.13 kB
dist-mobile/assets/MobileChatWorkspace-B3QtzgTW.js 27.90 kB │ gzip: 8.11 kB
dist-mobile/assets/RemoteTerminal-C8t4Pa3B.js 31.15 kB │ gzip: 10.90 kB
dist-mobile/assets/WelcomeWizard-EPL6nB2W.js 32.23 kB │ gzip: 9.52 kB
dist-mobile/assets/RemoteBrowserWorkspace-DC34mb7J.js 47.34 kB │ gzip: 13.44 kB
dist-mobile/assets/index-8l85aDrS.js 208.76 kB │ gzip: 66.27 kB
dist-mobile/assets/SettingsDialog-FwtXOrXs.js 288.21 kB │ gzip: 81.41 kB
dist-mobile/assets/RemoteApp-D_N4FfT1.js 543.75 kB │ gzip: 164.32 kB
dist-mobile/assets/App-BYycgCGu.js 950.58 kB │ gzip: 276.85 kB

(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
✓ built in 11.01s
Compiling ferryx-mobile v0.1.0 (<checkout>/mobile/src-tauri)
Finished `dev` profile [unoptimized] target(s) in 5.47s
Info symlinking lib "<checkout>/mobile/src-tauri/target/aarch64-linux-android/debug/libferryx_mobile_lib.so" in jniLibs dir "<checkout>/mobile/src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a"
Info "<checkout>/mobile/src-tauri/target/aarch64-linux-android/debug/libferryx_mobile_lib.so" requires shared lib "libandroid.so"
Info "<checkout>/mobile/src-tauri/target/aarch64-linux-android/debug/libferryx_mobile_lib.so" requires shared lib "libdl.so"
Info "<checkout>/mobile/src-tauri/target/aarch64-linux-android/debug/libferryx_mobile_lib.so" requires shared lib "liblog.so"
Info "<checkout>/mobile/src-tauri/target/aarch64-linux-android/debug/libferryx_mobile_lib.so" requires shared lib "libm.so"
Info "<checkout>/mobile/src-tauri/target/aarch64-linux-android/debug/libferryx_mobile_lib.so" requires shared lib "libc.so"
Info symlink at "<checkout>/mobile/src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a/libferryx_mobile_lib.so" points to "<checkout>/mobile/src-tauri/target/aarch64-linux-android/debug/libferryx_mobile_lib.so"
WARNING: We recommend using a newer Android Gradle plugin to use compileSdk = 36

This Android Gradle plugin (8.7.3) was tested up to compileSdk = 35.

You are strongly encouraged to update your project to use a newer
Android Gradle plugin that has been tested with compileSdk = 36.

If you are already using the latest version of the Android Gradle plugin,
you may need to wait until a newer version with support for compileSdk = 36 is available.

For more information refer to the compatibility table:
https://d.android.com/r/tools/api-level-support

To suppress this warning, add/update
android.suppressUnsupportedCompileSdk=36
to this project's gradle.properties.

> ferryx-mobile@0.1.0 tauri
> node scripts/tauri.mjs android android-studio-script --target aarch64

Info connection; remote_addr=127.0.0.1:39994 conn_id=0
Info Using installed NDK: <home>/android-sdk/ndk/27.2.12479018
Compiling ferryx-mobile v0.1.0 (<checkout>/mobile/src-tauri)
Finished `dev` profile [unoptimized] target(s) in 5.26s
Info symlinking lib "<checkout>/mobile/src-tauri/target/aarch64-linux-android/debug/libferryx_mobile_lib.so" in jniLibs dir "<checkout>/mobile/src-tauri/gen/android/app/src/main/jniLibs/arm64-v8a"
Info "<checkout>/mobile/src-tauri/target/aarch64-linux-android/debug/libferryx_mobile_lib.so" requires shared lib "libandroid.so"
Info "<checkout>/mobile/src-tauri/target/aarch64-linux-android/debug/libferryx_mobile_lib.so" requires shared lib "libdl.so"
Info "<checkout>/mobile/src-tauri/target/aarch64-linux-android/debug/libferryx_mobile_lib.so" requires shared lib "liblog.so"
Info "<checkout>/mobile/src-tauri/target/aarch64-linux-android/debug/libferryx_mobile_lib.so" requires shared lib "libm.so"
Info "<checkout>/mobile/src-tauri/target/aarch64-linux-android/debug/libferryx_mobile_lib.so" requires shared lib "libc.so"
Finished 1 APK at:
<checkout>/mobile/src-tauri/gen/android/app/build/outputs/apk/universal/debug/app-universal-debug.apk

15 changes: 15 additions & 0 deletions docs/evidence/mobile-tauri-20261019/apk-verification.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Verifies
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): false
Verified using v3.1 scheme (APK Signature Scheme v3.1): false
Verified using v4 scheme (APK Signature Scheme v4): false
Verified for SourceStamp: false
Number of signers: 1
package: name='com.ferryx.mobile' versionCode='1000' versionName='0.1.0' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15'
sdkVersion:'24'
targetSdkVersion:'35'
uses-permission: name='android.permission.INTERNET'
uses-permission: name='com.ferryx.mobile.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION'
native-code: 'arm64-v8a'
05c132f2fc4f48e4765865d9aa865cecfb00cb59abecdcd89fe685e4e5920664
4 changes: 4 additions & 0 deletions docs/evidence/mobile-tauri-20261019/coverage-summary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{"total": {"lines":{"total":69,"covered":69,"skipped":0,"pct":100},"statements":{"total":69,"covered":69,"skipped":0,"pct":100},"functions":{"total":11,"covered":11,"skipped":0,"pct":100},"branches":{"total":27,"covered":23,"skipped":0,"pct":85.18},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":"Unknown"}}
,"/home/sandbox/code/ferryx-wt/mobile-companion-20261019/ui/src/lib/clientRuntime.ts": {"lines":{"total":6,"covered":6,"skipped":0,"pct":100},"functions":{"total":2,"covered":2,"skipped":0,"pct":100},"statements":{"total":6,"covered":6,"skipped":0,"pct":100},"branches":{"total":3,"covered":3,"skipped":0,"pct":100}}
,"/home/sandbox/code/ferryx-wt/mobile-companion-20261019/ui/src/lib/settingsRuntimeBridge.ts": {"lines":{"total":63,"covered":63,"skipped":0,"pct":100},"functions":{"total":9,"covered":9,"skipped":0,"pct":100},"statements":{"total":63,"covered":63,"skipped":0,"pct":100},"branches":{"total":24,"covered":20,"skipped":0,"pct":83.33}}
}
Binary file added docs/evidence/mobile-tauri-20261019/phone-390.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions docs/evidence/mobile-tauri-20261019/remote-test-summary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"numTotalTests": 1221,
"numPassedTests": 1221,
"numFailedTests": 0,
"numPendingTests": 0,
"success": true
}
63 changes: 63 additions & 0 deletions mobile/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Ferryx mobile companion (Android + iOS)

A separate Tauri v2 shell embeds the existing RemoteApp bundle. It does not run
PTYs, the desktop daemon, native GPU terminals, or desktop filesystem commands.
Use an existing Ferryx desktop/relay to sign in and control remote sessions.
No native IPC capabilities or plugins are granted. Production network access is
HTTPS/WSS only; insecure LAN HTTP hosts are intentionally not supported yet.

## Build

Install Node, Rust, and the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/).
From the repository root install dependencies (`npm install`, then
`npm --prefix ui install`). Mobile uses the root Tauri CLI.

Android requires JDK 17, SDK platforms 35 and 36 (the Tauri runtime uses 36),
Build Tools 34/35, NDK 27.2.12479018, and JAVA_HOME/ANDROID_HOME/NDK_HOME.

```sh
cd mobile
npm run android:init
npm run android:build -- --debug --target aarch64 --apk
```

The init wrapper pins Gradle 8.9 and AGP 8.7.3 instead of unavailable preview
SDK defaults, and fixes the generated Node callback path. The Tauri runtime's
compileSdk 36 emits an AGP compatibility warning. Generated SDK projects are
ignored and recreated locally. Do not rerun raw `tauri android init` afterward.

On macOS with Xcode and iOS Rust targets:

```sh
cd mobile
npm run ios:init
npm run ios:dev
# Distribution also needs the owner's Apple team/signing setup.
npm run ios:build
```

iOS is configured, NOT compiled or run in this change. No Apple team or signing
identity is guessed, and no App Store/TestFlight deployment is performed.

Frontend-only validation: `npm --prefix ui run build:mobile`.
`npm --prefix ui run build` continues to build the desktop/web UI separately.

## QA status (2026-10-19)

- Mobile frontend and existing desktop frontend builds pass.
- ARM64 Android Rust native library cross-compiles successfully.
- Android ARM64 debug APK builds with the normal `android:build` command.
APK signature v2 verifies, package `com.ferryx.mobile` / version 0.1.0,
min SDK 24 / target SDK 35; ARM64 native library is present.
This is a debug-signed package, not a store/distribution release.
- 47 targeted tests pass (account sign-in/session, remote preferences, runtime
routing, settings). Runtime routing: 100% statements/branches/functions/lines.
Settings bridge: 100% statements/functions/lines, 83.33% branches. These are
focused-module numbers, not whole-project coverage.
- Production mobile bundle, with native internals present: login screen renders
without desktop IPC or JS errors at 360x800, 390x844 and 844x390, no horizontal
overflow. Landscape requires normal vertical scrolling.
- Complete remote UI suite: 1,221 tests pass, 0 fail, 0 skip (JSON reporter).
- No real phone/emulator, keyboard/IME, safe-area hardware, background/resume,
authenticated live relay, push notifications or iOS/Xcode QA has been done.
- Bundle-size warnings remain. This is a draft companion, not store-ready.
14 changes: 14 additions & 0 deletions mobile/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "ferryx-mobile",
"version": "0.1.0",
"private": true,
"scripts": {
"tauri": "node scripts/tauri.mjs",
"android:init": "node scripts/init-android.mjs",
"android:dev": "node scripts/tauri.mjs android dev",
"android:build": "node scripts/tauri.mjs android build",
"ios:init": "node scripts/tauri.mjs ios init",
"ios:dev": "node scripts/tauri.mjs ios dev",
"ios:build": "node scripts/tauri.mjs ios build"
}
}
28 changes: 28 additions & 0 deletions mobile/scripts/init-android.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import path from "node:path";

const mobile = fileURLToPath(new URL("..", import.meta.url));
const cli = path.join(mobile, "../node_modules/@tauri-apps/cli/tauri.js");
execFileSync(process.execPath, [cli, "android", "init", "--ci"], { cwd: mobile, stdio: "inherit" });
// The CLI's generated templates can drift independently of this source tree.
// Pin to the available Android 35/36 toolchain instead of silently requiring preview SDKs.
const gen = path.join(mobile, "src-tauri/gen/android");
function patch(file, transform) {
const target = path.join(gen, file);
writeFileSync(target, transform(readFileSync(target, "utf8")));
}
patch("build.gradle.kts", s => s.replace(/com.android.tools.build:gradle:[^"\s]+/, "com.android.tools.build:gradle:8.7.3"));
patch("buildSrc/build.gradle.kts", s => s.replace(/com.android.tools.build:gradle:[^"\s]+/, "com.android.tools.build:gradle:8.7.3"));
patch("gradle/wrapper/gradle-wrapper.properties", s => s.replace(/gradle-[\d.]+-bin.zip/, "gradle-8.9-bin.zip"));
patch("app/build.gradle.kts", s => s
.replace(/compileSdk = \d+/, "compileSdk = 35")
.replace(/targetSdk = \d+/, "targetSdk = 35")
.replace(/optimization\s*\{\s*enable = true\s*\}/, "isMinifyEnabled = true")
.replace(/lifecycle-process:[\d.]+/, "lifecycle-process:2.8.7"));
patch("gradle.properties", s => s
.replace(/^org.gradle.jvmargs=.*$/m, "org.gradle.jvmargs=-Xmx768m -XX:MaxMetaspaceSize=384m")
+ "\norg.gradle.workers.max=1\nkotlin.compiler.execution.strategy=in-process\norg.gradle.daemon=false\n");

patch("buildSrc/src/main/java/com/ferryx/mobile/kotlin/BuildTask.kt", s => s.replace('listOf("tauri", "android", "android-studio-script")', `listOf("${cli.replaceAll("\\", "/")}", "android", "android-studio-script")`));
10 changes: 10 additions & 0 deletions mobile/scripts/tauri.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
// Use an absolute CLI path so the generated Gradle callback works from src-tauri.
const cli = fileURLToPath(new URL("../../node_modules/@tauri-apps/cli/tauri.js", import.meta.url));
const result = spawnSync(process.execPath, [cli, ...process.argv.slice(2)], {
cwd: fileURLToPath(new URL("..", import.meta.url)),
stdio: "inherit",
});
if (result.error) throw result.error;
process.exit(result.status ?? 1);
Loading
Loading