Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4bd114eed | ||
|
|
7423e4d83b | ||
|
|
4b252a1d5d | ||
|
|
7148e1026e | ||
|
|
96f337ef46 |
@@ -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 • EfficientNetV2B0 • Transfer Learning</em>
|
||||
<em>Computer Vision • EfficientNetV2B0 • Rust ONNX Runtime • Tauri 2 Android</em>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<a href="#-jadwal"><b>Jadwal</b></a> •
|
||||
<a href="#-tech-stack"><b>Tech Stack</b></a> •
|
||||
<a href="#-memulai"><b>Memulai</b></a> •
|
||||
<a href="#-platform"><b>Platform</b></a> •
|
||||
<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 • Vite • TypeScript • React Router • TanStack Query • Zustand • Tailwind CSS
|
||||
**Tauri 2** (Android) • Rust • WebView • Deep Link OAuth
|
||||
|
||||
### Backend API
|
||||
Bun • Elysia • Drizzle ORM • PostgreSQL • prom-client
|
||||
|
||||
### Machine Learning
|
||||
Python • TensorFlow/Keras • EfficientNetV2B0 • Google Colab (GPU T4)
|
||||
Rust • Axum • ONNX Runtime • TFLite • TensorFlow.js
|
||||
|
||||
### Inference Engine
|
||||
**Rust** • **Axum** • **ONNX Runtime** • TFLite • TensorFlow.js
|
||||
|
||||
### DevOps & Infrastruktur
|
||||
Docker • Docker Compose • Coolify • Traefik • Tailscale • GitHub Actions (CI/CD)
|
||||
@@ -181,7 +187,8 @@ Prometheus • Metric Ingester (Go) • Vector • ClickHouse • Qu
|
||||
|
||||
- **Bun** — runtime & package manager
|
||||
- **Python 3.9–3.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` • `apps/tauri/gen/android/`
|
||||
|
||||
---
|
||||
|
||||
## 📚 Dokumentasi
|
||||
|
||||
| Dokumen | Isi |
|
||||
|
||||
@@ -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,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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user