Compare commits

...
5 Commits
Author SHA1 Message Date
MythEclipse b4bd114eed fix(tauri): configure deep-link plugin for mobile custom scheme 2026-06-16 05:14:08 +07:00
MythEclipseandClaude 7423e4d83b fix(tauri): use raw __TAURI_INTERNALS__ invoke, fix dynamic import on Android
Root cause: dynamic import('@tauri-apps/plugin-opener') silently fails on
Android Tauri WebView because the module resolution path for @tauri-apps/api
(plugin dependency) differs from npm expectations in the bundled context.

Rewrote tauri.ts to use window.__TAURI_INTERNALS__.invoke() directly:
- openUrl() → invoke('plugin:opener|open_url', {url})
- setupDeepLinkHandler() → invoke('plugin:deep-link|get_current')
- Warm-start listener still uses import('@tauri-apps/api/event') for
  deep-link://new-url events (bundled as separate chunk by Vite)
- Added @tauri-apps/api as direct dependency

Also kept withGlobalTauri: true (needed for __TAURI_INTERNALS__ injection)
but reverted APK frontendDist back to bundled React app (../web/dist)
since redirect-to-live-web approach was unreliable.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 04:34:13 +07:00
MythEclipseandClaude 4b252a1d5d fix(tauri): enable withGlobalTauri so Tauri APIs work on live web
Without this, the redirect page loads zeavisedu.asepharyana.my.id but
__TAURI_INTERNALS__ is not injected, so isTauri() returns false,
handleGoogleLogin uses window.location.href (WebView navigation), and
Google blocks embedded WebView since 2016.

With withGlobalTauri=true, the live web page detects Tauri, uses tauri-
plugin-opener to open system browser, and Google OAuth works.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 04:01:38 +07:00
MythEclipseandClaude 7148e1026e feat(android): APK loads live web, fix cold-start deep links for Google OAuth
APK architecture change: replaces bundled React SPA with minimal redirect
page that always loads live web content. No more APK rebuilds for web changes.

Root cause of OAuth failure on Android:
1. Cold-start deep links lost — APK's old bundled JS called onOpenUrl()
   (warm-start listener only) but NOT getCurrent() which is required for
   cold-start deep links. Fix: redirect page + setupDeepLinkHandler both
   call getCurrent() before redirecting/navigating.
2. Session cookie was dropped — renderTauriDeepLinkPage returned a raw
   new Response() which overwrote the Set-Cookie header set by the
   callback handler. Fix: inject Set-Cookie into the Response.
3. tauri.conf.json frontendDist → "./web/dist-tauri" (redirect page)
4. Added @tauri-apps/plugin-deep-link and @tauri-apps/plugin-opener as
   web app deps so live-web imports work in Tauri WebView.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 03:47:22 +07:00
MythEclipseandClaude 96f337ef46 docs: add Tauri 2 Android and Rust ONNX Runtime sections
- New 'Platform' section detailing Tauri 2 Android app (WebView, Deep Link OAuth, Camera)
- Add Android CI/CD info, build commands, and GitHub Actions workflow reference
- Tagline updated to include 'Rust ONNX Runtime' and 'Tauri 2 Android'
- Architecture tree now shows apps/tauri/
- Components table: Android App row with Tauri 2, Rust, WebView, Deep Link OAuth
- Tech Stack: separate 'Inference Engine' section with Rust/Axum/ONNX Runtime bolded
- Prerequisites: Java 21 + Android SDK added
- Dev commands: tauri dev and tauri android dev included
- Cakupan updated: Web + Android (Tauri 2) now in-scope

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 02:58:52 +07:00
8 changed files with 177 additions and 47 deletions
+58 -9
View File
@@ -4,7 +4,7 @@
<h1 align="center">ZeaVis Edu</h1>
<p align="center">
<strong>Asisten Edukasi Interaktif untuk Deteksi Penyakit Daun Jagung</strong><br>
<em>Computer Vision &bull; EfficientNetV2B0 &bull; Transfer Learning</em>
<em>Computer Vision &bull; EfficientNetV2B0 &bull; Rust ONNX Runtime &bull; Tauri 2 Android</em>
</p>
</p>
@@ -16,6 +16,7 @@
<a href="#-jadwal"><b>Jadwal</b></a> &bull;
<a href="#-tech-stack"><b>Tech Stack</b></a> &bull;
<a href="#-memulai"><b>Memulai</b></a> &bull;
<a href="#-platform"><b>Platform</b></a> &bull;
<a href="#-dokumentasi"><b>Dokumentasi</b></a>
</p>
@@ -79,7 +80,7 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
| Klasifikasi 3 penyakit + 1 daun sehat | Penyakit pada batang atau buah jagung |
| Deteksi berbasis unggah gambar daun | Prediksi tanpa input gambar |
| Rekomendasi obat & penanganan | Diagnosis pengganti ahli/POPT |
| Aplikasi web edukatif | Aplikasi mobile native |
| Aplikasi Web + Android (Tauri 2) | Aplikasi iOS |
### 4 Kelas yang Diklasifikasikan
@@ -98,7 +99,7 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
| 2 | **Model ML** | Model Computer Vision terlatih di Google Colab, siap produksi |
| 3 | **UI Antarmuka** | Front-End berbasis React + Vite dengan fitur unggah gambar |
| 4 | **Back-End Integration** | API + ML Service untuk inferensi real-time via Docker |
| 5 | **Prototipe Akhir** | Aplikasi web final dengan klasifikasi + modul edukasi (rekomendasi obat & penanganan) |
| 5 | **Prototipe Akhir** | Aplikasi Web + Android (Tauri 2) dengan klasifikasi & modul edukasi (rekomendasi obat & penanganan) |
---
@@ -131,7 +132,8 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
.
├── apps/
│ ├── api/ # Backend Elysia/Bun + Drizzle ORM + PostgreSQL
│ ├── ml-service/ # Rust/Axum + ONNX Runtime inference service
│ ├── ml-service/ # Rust/Axum + ONNX Runtime inference engine
│ ├── tauri/ # Tauri 2 mobile wrapper → Android APK
│ └── web/ # Frontend React + Vite + Tailwind CSS
├── Machine_Learning/ # Pipeline dataset, training Colab, ekspor model
│ └── README.md # ⤷ Panduan lengkap pipeline ML
@@ -147,8 +149,9 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
| Komponen | Teknologi | Dokumentasi |
|---|---|---|
| Web Frontend | React, Vite, Tailwind, Zustand, TanStack Query | `apps/web/` |
| Android App | Tauri 2, Rust, WebView, Deep Link OAuth | `apps/tauri/` |
| API Backend | Bun, Elysia, Drizzle ORM, PostgreSQL | `apps/api/` |
| ML Inference | Rust, Axum, ONNX Runtime | [`apps/ml-service/README.md`](apps/ml-service/README.md) |
| ML Inference Engine | Rust, Axum, ONNX Runtime | [`apps/ml-service/README.md`](apps/ml-service/README.md) |
| ML Pipeline | Python, TensorFlow/Keras, EfficientNetV2B0 | [`Machine_Learning/README.md`](Machine_Learning/README.md) |
| Infrastruktur | Docker, Coolify, Traefik, Tailscale | [`infra/README.md`](infra/README.md) |
| Telemetry | Prometheus, ClickHouse, Vector, Vue 3 | `telemetry/` |
@@ -157,15 +160,18 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
## 🛠️ Tech Stack
### Frontend
### Frontend & Mobile
React &bull; Vite &bull; TypeScript &bull; React Router &bull; TanStack Query &bull; Zustand &bull; Tailwind CSS
**Tauri 2** (Android) &bull; Rust &bull; WebView &bull; Deep Link OAuth
### Backend API
Bun &bull; Elysia &bull; Drizzle ORM &bull; PostgreSQL &bull; prom-client
### Machine Learning
Python &bull; TensorFlow/Keras &bull; EfficientNetV2B0 &bull; Google Colab (GPU T4)
Rust &bull; Axum &bull; ONNX Runtime &bull; TFLite &bull; TensorFlow.js
### Inference Engine
**Rust** &bull; **Axum** &bull; **ONNX Runtime** &bull; TFLite &bull; TensorFlow.js
### DevOps & Infrastruktur
Docker &bull; Docker Compose &bull; Coolify &bull; Traefik &bull; Tailscale &bull; GitHub Actions (CI/CD)
@@ -181,7 +187,8 @@ Prometheus &bull; Metric Ingester (Go) &bull; Vector &bull; ClickHouse &bull; Qu
- **Bun** — runtime & package manager
- **Python 3.93.11** — pipeline ML
- **Rust & Cargo** — `apps/ml-service`
- **Rust & Cargo** — `apps/ml-service` (inference) & `apps/tauri` (Android)
- **Java 21 + Android SDK** — build Android APK
- **Docker & Docker Compose** — deployment & telemetry
- **PostgreSQL** — backend API
@@ -199,7 +206,9 @@ bun install
bun run dev # Semua service (web + api)
cd apps/web && bun run dev # Hanya frontend
cd apps/api && bun run start # Hanya backend API
cd apps/ml-service && cargo run # Hanya ML service (port 8000)
cd apps/ml-service && cargo run # ML inference engine (port 8000)
cd apps/tauri && bun run tauri dev # Tauri desktop dev
cd apps/tauri && bun run tauri android dev # Tauri Android dev
```
### Environment Variables
@@ -236,6 +245,46 @@ make telemetry-up # Telemetry stack
---
## 📱 Platform
ZeaVis Edu tersedia di **dua platform** dari satu codebase:
| Platform | Teknologi | Build |
|---|---|---|
| **Web** | React + Vite → Static SPA | `bun run build` |
| **Android** | Tauri 2 + Rust → WebView APK | `cd apps/tauri && bun run tauri android build --apk` |
### Tauri 2 Android
Aplikasi Android membungkus frontend web yang sama dalam **WebView native** menggunakan **Tauri 2**, memberikan akses ke API native Android tanpa menulis ulang UI.
**Fitur Android:**
- **Google OAuth** — Login via system browser + deep link `zeavisedu://` kembali ke app
- **Kamera** — Izin `CAMERA` untuk unggah foto daun jagung langsung dari kamera
- **Tauri Plugin Opener** — Buka URL eksternal di system browser
- **Tauri Plugin Deep Link** — Tangkap OAuth callback tanpa memerlukan server redirect
**CI/CD Android:**
- GitHub Actions workflow `.github/workflows/android.yml`
- Build otomatis di setiap push/PR ke `main`
- Patch `AndroidManifest.xml` untuk menambahkan izin kamera + intent filter deep link
- APK ditandatangani (signed) via `apksigner` + release ke GitHub Releases
```bash
# Development Android (butuh Android SDK + emulator/device)
cd apps/tauri
bun run tauri android init # Init project Android
bun run tauri android dev # Dev dengan hot reload
bun run tauri android build --apk # Build APK production
# CI/CD — dijalankan otomatis via GitHub Actions
.github/workflows/android.yml
```
> Konfigurasi: `apps/tauri/tauri.conf.json` &bull; `apps/tauri/gen/android/`
---
## 📚 Dokumentasi
| Dokumen | Isi |
+8 -2
View File
@@ -314,12 +314,18 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
}
const token = await createSession(user.id);
set.headers['Set-Cookie'] = createSessionCookie(token, request.headers);
const sessionCookie = createSessionCookie(token, request.headers);
authCounter.labels('login', 'true').inc();
const successUrl = `${env.webAppUrl}/login?token=${encodeURIComponent(token)}`;
if (platform === 'tauri') return renderTauriDeepLinkPage(successUrl);
if (platform === 'tauri') {
// Inject Set-Cookie into the response so the browser gets it on redirect
const resp = renderTauriDeepLinkPage(successUrl);
resp.headers.set('Set-Cookie', sessionCookie);
return resp;
}
set.headers['Set-Cookie'] = sessionCookie;
set.status = 302;
set.headers['Location'] = successUrl;
} catch (err) {
+10 -2
View File
@@ -10,7 +10,7 @@
"beforeDevCommand": "cd ../web && bun run dev"
},
"app": {
"withGlobalTauri": false,
"withGlobalTauri": true,
"windows": [
{
"title": "ZeaVis Edu",
@@ -26,5 +26,13 @@
"active": true,
"targets": "all"
},
"plugins": {}
"plugins": {
"deep-link": {
"mobile": [
{
"scheme": ["zeavisedu"]
}
]
}
}
}
+44
View File
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ZeaVis Edu</title>
<script>
var LIVE = 'https://zeavisedu.asepharyana.my.id';
var T = window.__TAURI_INTERNALS__;
function navigate(path) {
window.location.replace(LIVE + path);
}
// On cold start, check if the app was opened via a deep link (Google OAuth)
// before redirecting to the live web app.
if (T && T.invoke) {
T.invoke('plugin:deep-link|get_current')
.then(function(urls) {
if (urls && urls.length > 0 && urls[0]) {
try {
var u = new URL(urls[0]);
var target = u.pathname + u.search + u.hash;
if (target && target !== '/') {
// Preserve full path + query (e.g. /login?token=xxx)
navigate(target);
return;
}
} catch (e) { /* malformed URL — fall through */ }
}
// No deep link — redirect to live app home
navigate('/');
})
.catch(function() { navigate('/'); });
} else {
// Not in Tauri (dev mode or unknown) — redirect to live
navigate('/');
}
</script>
</head>
<body style="background:#f0fdf4;font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0">
<p style="color:#16a34a">Memuat ZeaVis Edu...</p>
</body>
</html>
+2
View File
@@ -12,6 +12,8 @@
"dependencies": {
"@radix-ui/react-slot": "^1.2.4",
"@tanstack/react-query": "^5.100.11",
"@tauri-apps/plugin-deep-link": "^2",
"@tauri-apps/plugin-opener": "^2",
"@vitejs/plugin-react": "^6.0.2",
"@zeavis/shared": "workspace:*",
"class-variance-authority": "^0.7.1",
+51 -34
View File
@@ -1,7 +1,6 @@
/**
* Lightweight Tauri environment detection and utilities.
* Avoids importing @tauri-apps/api at module level so the web build
* doesn't bundle Tauri internals.
* Uses raw __TAURI_INTERNALS__ IPC to avoid bundling/import issues on Android.
*/
let _isTauri: boolean | null = null;
@@ -14,47 +13,65 @@ export function isTauri(): boolean {
return _isTauri;
}
/** Get the Tauri IPC invoke function directly from the global internals. */
function tauriInvoke(): (cmd: string, args?: Record<string, unknown>) => Promise<unknown> {
const T = (window as any).__TAURI_INTERNALS__;
if (!T?.invoke) throw new Error('Tauri IPC not available');
return T.invoke.bind(T);
}
export async function openUrl(url: string): Promise<void> {
if (!isTauri()) {
// Not in Tauri — normal browser navigation
window.location.href = url;
return;
}
const { openUrl: tauriOpenUrl } = await import('@tauri-apps/plugin-opener');
await tauriOpenUrl(url);
try {
const invoke = tauriInvoke();
await invoke('plugin:opener|open_url', { url });
} catch (err) {
console.error('Tauri openUrl failed, trying fallback:', err);
// Fallback: navigate the WebView (Google will block, but best effort)
window.location.href = url;
}
}
function processDeepLinkUrl(url: string): void {
try {
const u = new URL(url);
const target = u.pathname + u.search + u.hash;
if (target && target !== '/') {
window.location.href = target;
return;
}
} catch { /* fall through */ }
// Fallback: handle both :// and :/ custom schemes
let match = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
if (!match) match = url.match(/^[^:]+:\/(\/.*)?$/);
if (match?.[1]) window.location.href = match[1];
}
/**
* Listen for deep link URLs when the app is opened from an external link.
* On Android, after Google OAuth completes in the system browser, the
* callback page redirects to zeavisedu://... which triggers this listener.
* We extract the path + query and navigate the WebView there.
*/
export async function setupDeepLinkHandler(): Promise<void> {
if (!isTauri()) return;
const { onOpenUrl } = await import('@tauri-apps/plugin-deep-link');
onOpenUrl((urls) => {
for (const url of urls) {
// url looks like: zeavisedu://login/login?token=xxx
// Extract path + query after the host
try {
const u = new URL(url);
const target = u.pathname + u.search + u.hash;
if (target && target !== '/') {
window.location.href = target;
return;
}
} catch { /* try fallback below */ }
try {
const invoke = tauriInvoke();
// Fallback: extract everything after scheme, handling both // and /
let match = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
if (!match) {
// Also handle single-slash non-hierarchical URLs (e.g. zeavisedu:/path)
match = url.match(/^[^:]+:\/(\/.*)?$/);
}
if (match?.[1]) {
window.location.href = match[1];
}
}
});
// Cold-start: app just opened via intent:// or custom scheme
invoke('plugin:deep-link|get_current')
.then((urls: any) => {
if (urls?.[0]) processDeepLinkUrl(urls[0]);
})
.catch(() => { /* plugin may not be registered yet */ });
// Warm-start: listen for new URLs while app is running
const { listen } = await import('@tauri-apps/api/event');
listen('deep-link://new-url', (event: any) => {
const urls = event.payload as string[];
for (const url of urls) processDeepLinkUrl(url);
});
} catch (err) {
console.error('Tauri deep-link setup failed:', err);
}
}
+3
View File
@@ -5,6 +5,7 @@
"": {
"name": "zeavis-edu",
"dependencies": {
"@tauri-apps/api": "2.11.0",
"@tauri-apps/plugin-deep-link": "2.4.9",
"@tauri-apps/plugin-opener": "2.5.4",
},
@@ -53,6 +54,8 @@
"dependencies": {
"@radix-ui/react-slot": "^1.2.4",
"@tanstack/react-query": "^5.100.11",
"@tauri-apps/plugin-deep-link": "^2",
"@tauri-apps/plugin-opener": "^2",
"@vitejs/plugin-react": "^6.0.2",
"@zeavis/shared": "workspace:*",
"class-variance-authority": "^0.7.1",
+1
View File
@@ -15,6 +15,7 @@
"packages/*"
],
"dependencies": {
"@tauri-apps/api": "2.11.0",
"@tauri-apps/plugin-deep-link": "2.4.9",
"@tauri-apps/plugin-opener": "2.5.4"
}