Compare commits

...
5 Commits
Author SHA1 Message Date
MythEclipseandClaude 1afd2e013c fix(android): add deep link handler to intercept zeavisedu:// URLs and navigate WebView
Login-page won't auto-process token on deep link return because
the WebView stays on the page it was on. Added setupDeepLinkHandler()
which listens for zeavisedu:// scheme URLs and navigates the WebView
to the correct path+query.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 01:03:45 +07:00
MythEclipseandClaude e1732f5111 docs: enrich root README with full project plan details
- Add project plan context: team members, executive summary, schedule, risk management
- Add project scope, deliverables, and classification table with symptoms
- Add bibliography/references section from project plan
- Centered header with logo, emoji-section navigation
- Cleaner structure: Tentang → Tim → Ringkasan → Cakupan → Jadwal → Risiko → Arsitektur → Tech Stack → Memulai → Dokumentasi → Pustaka

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 00:25:47 +07:00
MythEclipseandClaude fc832b404e fix(auth): use VITE_API_BASE_URL for Google OAuth redirect instead of window.location.origin
On Tauri Android, window.location.origin = http://tauri.localhost
which is the embedded dev server URL, not the API server.
Use VITE_API_BASE_URL env var which points to the production API.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 00:23:46 +07:00
MythEclipseandClaude 6ba3deb149 docs: restructure all README.md into cohesive hierarchy
- Root README redesigned as landing page with 7 sub-chapters
- Each child README gets navigation header + footer linking back to root
- Cross-links between Machine_Learning, ml-service, and infra READMEs
- Reduced duplication: root summarizes, children provide full detail
- Net -207 lines, cleaner structure

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 00:23:18 +07:00
MythEclipseandClaude ac59548337 feat(android): Google OAuth via system browser + deep link for Android
Background: Google blocks OAuth in embedded WebView (403 disallowed_useragent).
Solution: open Google login in the Android system browser, then deep-link
back to the Tauri app via custom scheme after callback.

Changes:
- Tauri: add tauri-plugin-opener + tauri-plugin-deep-link to Cargo.toml
- Tauri: register plugins in lib.rs, add capabilities
- Web: auth-form.tsx Google button uses openUrl() via @tauri-apps/plugin-opener
  on Tauri (opens in system browser), falls back to window.location.href
- Web: add lib/tauri.ts for isTauri() detection + lazy opens
- API: /auth/google accepts ?platform=tauri → encodes into OAuth state param
- API: /auth/google/callback decodes state → if tauri, renders HTML page
  that deep-links back via zeavisedu:// scheme; if web, 302 redirect
- Android: patch script adds deep link intent filter for zeavisedu:// scheme

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-15 23:49:47 +07:00
15 changed files with 630 additions and 662 deletions
+25
View File
@@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" fill="none">
<!-- Background circle -->
<circle cx="64" cy="64" r="62" fill="#166534" stroke="#22c55e" stroke-width="3"/>
<circle cx="64" cy="64" r="56" fill="#15803d"/>
<!-- Corn leaf shape -->
<path d="M38 75 C32 55 30 35 45 22 C55 14 68 10 80 18 C85 22 90 26 95 30
C98 34 96 38 90 36 C84 34 78 32 72 34
C74 38 78 42 78 48 C78 56 72 62 64 64
C56 66 50 70 44 76 C42 78 38 80 38 75Z"
fill="#86efac" opacity="0.9"/>
<!-- Leaf vein -->
<path d="M46 56 C52 52 60 48 68 50" stroke="#166534" stroke-width="1.5" fill="none" opacity="0.6"/>
<path d="M50 62 C56 58 62 55 68 56" stroke="#166534" stroke-width="1.5" fill="none" opacity="0.6"/>
<!-- Magnifying glass / AI overlay -->
<circle cx="78" cy="68" r="18" fill="none" stroke="#fbbf24" stroke-width="3"/>
<line x1="91" y1="81" x2="98" y2="88" stroke="#fbbf24" stroke-width="3" stroke-linecap="round"/>
<!-- AI sparkle dots -->
<circle cx="70" cy="60" r="2" fill="#fbbf24"/>
<circle cx="82" cy="56" r="1.5" fill="#fbbf24"/>
<circle cx="76" cy="74" r="2" fill="#fbbf24"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+9 -3
View File
@@ -1,6 +1,8 @@
# Corn Leaf Disease Classification # Pipeline Machine Learning — ZeaVis Edu
Pipeline lengkap untuk klasifikasi penyakit daun jagung menggunakan **EfficientNetV2B0**, mulai dari persiapan dataset, pelatihan di Google Colab, hingga ekspor model ke format **TFLite** dan **TensorFlow.js** untuk kebutuhan produksi. > Panduan lengkap: preprocessing dataset, pelatihan di Google Colab, ekspor model ke TFLite, TensorFlow.js, dan ONNX.
← [Kembali ke README utama](../README.md)
--- ---
@@ -390,4 +392,8 @@ export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
--- ---
### Sesi Colab terputus saat training ### Sesi Colab terputus saat training
**Solusi:** Gunakan callback `ModelCheckpoint` di notebook untuk menyimpan checkpoint secara berkala ke Google Drive, sehingga training bisa dilanjutkan dari checkpoint terakhir tanpa mengulang dari awal. **Solusi:** Gunakan callback `ModelCheckpoint` di notebook untuk menyimpan checkpoint secara berkala ke Google Drive, sehingga training bisa dilanjutkan dari checkpoint terakhir tanpa mengulang dari awal.
---
← [Kembali ke README utama](../README.md) &bull; [ML Service →](../apps/ml-service/README.md)
+220 -425
View File
@@ -1,482 +1,277 @@
# ZeaVis Edu <p align="center">
<br>
<img src=".github/assets/zeavis-logo.svg" alt="ZeaVis Edu" width="128"><br>
<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>
</p>
</p>
ZeaVis Edu adalah aplikasi edukasi untuk membantu mengenali penyakit daun jagung melalui klasifikasi gambar berbasis machine learning. Repositori ini menggabungkan aplikasi web, API backend, layanan inferensi ML, serta pipeline pelatihan dan ekspor model EfficientNetV2B0. <p align="center">
<a href="#-tentang"><b>Tentang</b></a> &bull;
<a href="#-tim"><b>Tim</b></a> &bull;
<a href="#-ringkasan-eksekutif"><b>Ringkasan</b></a> &bull;
<a href="#-cakupan--deliverables"><b>Cakupan</b></a> &bull;
<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="#-dokumentasi"><b>Dokumentasi</b></a>
</p>
## Fitur Utama <br>
- Aplikasi web untuk pengalaman pengguna dan interaksi edukatif. ---
- API backend untuk status layanan, integrasi data, dan komunikasi dengan layanan ML.
- ML service berbasis Rust/Axum dengan ONNX Runtime untuk inferensi penyakit daun jagung dari gambar.
- Pipeline machine learning untuk preprocessing dataset, training di Google Colab, dan ekspor model produksi.
- Dukungan Docker untuk deployment web, API, dan ML service.
- Workspace monorepo berbasis Bun dan Moon untuk menjalankan task development, typecheck, dan build secara terpusat.
## Kelas Penyakit ## 🌽 Tentang
Model klasifikasi menargetkan empat label berbahasa Indonesia: **ZeaVis Edu** adalah aplikasi edukasi berbasis **Computer Vision** yang membantu petani, mahasiswa pertanian, dan penyuluh lapangan mengidentifikasi penyakit daun jagung secara mandiri — cukup dengan mengunggah foto daun jagung.
| Label | Deskripsi | Proyek ini merupakan **Capstone Project** dalam program **Pijak × IBM SkillsBuild** dengan tema **"AI for Smart Education"**, dirancang untuk menjembatani kesenjangan antara pengetahuan teori pertanian dan kebutuhan praktis di lapangan.
---
## 👥 Tim
| NPM | Nama | Learning Path | Peran |
|---|---|---|---|
| APC246D6Y0028 | **Asep Haryana Saputra** | Back-End | Arsitektur sistem, RESTful API, deployment Docker/Cloud, keamanan upload stream |
| APC013D6X0081 | **Selly Supriyatin** | Front-End | UI/UX responsif, mekanisme unggah gambar, modul edukasi (rekomendasi obat & penanganan) |
| APC013D6Y0091 | **Taufik Pathurrohman** | Machine Learning | Data Engineering — ekstraksi dataset, cleaning, augmentasi gambar |
| APC414D6Y0138 | **Luhung Pandyaska Suyi** | Machine Learning | Model Architecture & Training — CNN, hyperparameter tuning |
| APC013D6Y0269 | **Ardian** | Machine Learning | Model Evaluation & Deployment Prep — confusion matrix, konversi ke production-ready |
---
## 📋 Ringkasan Eksekutif
### Masalah
Data BPS menunjukkan penurunan luas panen jagung dari **2.764.366 Ha (2022)** menjadi **2.487.191 Ha (2023)**. Salah satu penyebab utamanya adalah penyakit daun seperti **Hawar Daun**, **Karat Daun**, dan **Bercak Daun Abu-abu** yang menyebabkan nekrosis dan menghambat fotosintesis.
Petani sering kesulitan mengidentifikasi penyakit secara kasat mata dan memiliki **ketergantungan tinggi pada POPT** (Petugas Pengendali Organisme Pengganggu Tumbuhan) akibat minimnya media pembelajaran interaktif.
### Solusi
ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
1. 📸 **Unggah** foto daun jagung yang diduga terinfeksi
2. 🤖 **Deteksi otomatis** penyakit oleh model AI (EfficientNetV2B0)
3. 📚 **Dapatkan** informasi detail penyakit, panduan pencegahan, dan rekomendasi obat secara mandiri
### Metode Teknis
| Komponen | Pilihan |
|---|---| |---|---|
| Bercak Daun | Gray Leaf Spot | | Arsitektur Model | **EfficientNetV2B0** — keseimbangan optimal antara akurasi dan efisiensi parameter |
| Hawar Daun | Northern/Southern Leaf Blight | | Metode Pelatihan | **Transfer Learning** pada Google Colab (GPU T4) |
| Karat Daun | Common Rust | | Sumber Dataset | Kaggle — [Corn Leaf Disease](https://www.kaggle.com/datasets/ndisan/corn-leaf-disease) |
| Daun Sehat | Daun jagung tanpa gejala penyakit | | Deployment | VPS dengan Docker, ONNX Runtime untuk inferensi real-time |
## Struktur Proyek ---
```text ## 🎯 Cakupan & Deliverables
### Cakupan
| ✅ Dalam Cakupan | ❌ Di Luar Cakupan |
|---|---|
| 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 |
### 4 Kelas yang Diklasifikasikan
| Label | Nama Ilmiah | Gejala |
|---|---|---|
| **Hawar Daun** | *Northern/Southern Leaf Blight* | Hawar coklat memanjang pada daun |
| **Karat Daun** | *Common Rust* | Bintik coklat kemerahan berbentuk pustula |
| **Bercak Daun** | *Gray Leaf Spot* | Bercak abu-abu memanjang |
| **Daun Sehat** | — | Tanpa gejala penyakit |
### Deliverables Proyek
| No | Tahapan | Deskripsi |
|---|---|---|
| 1 | **Pengumpulan Data** | Dataset gambar 3 penyakit + 1 daun sehat dari Kaggle beserta pelabelan |
| 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) |
---
## 📅 Jadwal
| Minggu | Tanggal | Fase | Aktivitas |
|---|---|---|---|
| **1** | 1117 Mei 2026 | Inisiasi & Data | Spesifikasi teknis (Asep) • Dataset dari Kaggle + preprocessing (Taufik) • Wireframe UI/UX (Selly) |
| **2** | 1824 Mei 2026 | Training & Dev Awal | Implementasi EfficientNetV2B0 di Colab (Luhung) • Slicing UI ke React (Selly) • Setup server, database, routing API (Asep) |
| **3** | 2531 Mei 2026 | Evaluasi & Modul Edukasi | Evaluasi akurasi + konversi model ke ONNX/TFLite (Ardian) • Halaman edukasi obat & penanganan (Selly) • RESTful API untuk image upload & inferensi (Asep) |
| **4** | 17 Juni 2026 | Integrasi & Testing | Integrasi penuh Front-End ↔ API ↔ Model ML • Pengujian end-to-end • Stress testing & error handling (Semua) |
| **5** | 814 Juni 2026 | Deployment & Finalisasi | Deployment ke VPS (Asep) • Bug fixing & optimalisasi UI/UX (Selly) • Dokumentasi teknis & materi presentasi (Semua) |
---
## ⚠️ Manajemen Risiko
| Risiko | Solusi |
|---|---|
| **Overfitting akibat imbalanced data** | Augmentasi tingkat lanjut (kecerahan, noise, rotasi) + confidence threshold < 75% → minta user foto ulang |
| **Server downtime / latensi tinggi** | Batasan upload ≤ 5 MB + kompresi server-side + rate limiting + container Docker isolasi resource |
| **Foto blur / objek bukan daun jagung** | Panduan visual (overlay) pada UI + validasi anomali + disclaimer "alat bantu edukasi, bukan pengganti POPT" |
| **Bottleneck integrasi ML ↔ API ↔ UI** | API Contract ketat di minggu ke-1 + integrasi bertahap (CI) mulai minggu ke-3 |
---
## 🏗️ Arsitektur Proyek
```
. .
├── apps/ ├── apps/
│ ├── api/ # Backend Elysia/Bun │ ├── api/ # Backend Elysia/Bun + Drizzle ORM + PostgreSQL
│ ├── ml-service/ # Layanan inferensi Rust/Axum + ONNX Runtime │ ├── ml-service/ # Rust/Axum + ONNX Runtime inference service
│ └── web/ # Frontend React + Vite │ └── web/ # Frontend React + Vite + Tailwind CSS
├── Machine_Learning/ # Pipeline dataset, training, dan ekspor model ├── Machine_Learning/ # Pipeline dataset, training Colab, ekspor model
├── packages/ │ └── README.md # ⤷ Panduan lengkap pipeline ML
│ └── shared/ # Tipe dan utilitas bersama TypeScript ├── infra/
├── docker-compose.yml # Konfigurasi deployment container │ └── README.md # ⤷ Panduan deployment multi-VPS
├── package.json # Script dan workspace root Bun ├── packages/shared/ # Tipe & utilitas TypeScript bersama
── README.md # Dokumentasi utama proyek ── telemetry/ # Submodule — Prometheus → ClickHouse pipeline
├── docker-compose.yml # Konfigurasi deployment container
├── package.json # Root workspace Bun + Moon
└── README.md # ⤷ Anda di sini
``` ```
## Tech Stack | Komponen | Teknologi | Dokumentasi |
|---|---|---|
| Web Frontend | React, Vite, Tailwind, Zustand, TanStack Query | `apps/web/` |
| 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 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/` |
---
## 🛠️ Tech Stack
### Frontend ### Frontend
React &bull; Vite &bull; TypeScript &bull; React Router &bull; TanStack Query &bull; Zustand &bull; Tailwind CSS
- React
- Vite
- TypeScript
- React Router
- TanStack Query
- Zustand
- Tailwind CSS
### Backend API ### Backend API
Bun &bull; Elysia &bull; Drizzle ORM &bull; PostgreSQL &bull; prom-client
- Bun
- Elysia
- Drizzle ORM
- PostgreSQL
### Machine Learning ### Machine Learning
Python &bull; TensorFlow/Keras &bull; EfficientNetV2B0 &bull; Google Colab (GPU T4)
Rust &bull; Axum &bull; ONNX Runtime &bull; TFLite &bull; TensorFlow.js
- Python (preprocessing, training, export) ### DevOps & Infrastruktur
- TensorFlow/Keras Docker &bull; Docker Compose &bull; Coolify &bull; Traefik &bull; Tailscale &bull; GitHub Actions (CI/CD)
- EfficientNetV2B0
- Rust
- Axum
- ONNX Runtime
- TFLite
- TensorFlow.js
### Tooling & Deployment ### Observabilitas
Prometheus &bull; Metric Ingester (Go) &bull; Vector &bull; ClickHouse &bull; Query Proxy (Go) &bull; Telemetry UI (Vue 3)
- Bun workspaces ---
- Moon task runner
- Docker
- Docker Compose
- GitHub Container Registry
- Traefik labels untuk routing deployment
### Telemetry & Observability ## 🚀 Memulai
- Prometheus — metric scraping & remote_write ### Prasyarat
- Metric Ingester (Go) — enrichment, filtering, aggregation
- Vector — buffering & backpressure
- ClickHouse — columnar analytical storage
- Query Proxy (Go) — read-only SQL proxy
- Telemetry UI (Vue 3) — metrics dashboard
- Semua service ZeaVis Edu (web, api, ml-service) mengekspos metrik Prometheus di `/metrics`
- Client-side Web Vitals (CLS, FCP, INP, LCP, TTFB) dikumpulkan di frontend
## Prasyarat - **Bun** — runtime & package manager
- **Python 3.93.11** — pipeline ML
- **Rust & Cargo** — `apps/ml-service`
- **Docker & Docker Compose** — deployment & telemetry
- **PostgreSQL** — backend API
Untuk menjalankan seluruh project secara lokal, siapkan: ### Instalasi
- Bun
- Python 3.93.11 untuk pipeline ML
- Rust dan Cargo untuk `apps/ml-service`
- Docker dan Docker Compose jika ingin menjalankan/deploy via container
- PostgreSQL jika fitur backend yang membutuhkan database digunakan
- File model `Machine_Learning/model/model.onnx` untuk inferensi ML lokal
## Instalasi Root Workspace
Jalankan dari root repository:
```bash ```bash
git clone https://github.com/ATLAS-PJK-GM007/ZeaVis-Edu.git
cd ZeaVis-Edu
bun install bun install
``` ```
## Menjalankan Project Lokal ### Menjalankan Development
### Menjalankan Semua Task Development
```bash ```bash
bun run dev 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)
``` ```
Script ini menjalankan task `dev` melalui Moon untuk workspace yang tersedia. ### Environment Variables
### Type Check Salin `.env.example` ke `.env` dan isi:
```bash | Variable | Keterangan |
bun run typecheck
```
### Build Produksi
```bash
bun run build
```
## Menjalankan Service Secara Terpisah
### Web App
```bash
cd apps/web
bun run dev
```
Secara default Vite akan menjalankan server development dan menampilkan URL lokal di terminal.
### API Backend
```bash
cd apps/api
bun run start
```
API membaca konfigurasi dari file `.env` di root repository melalui script Bun.
Script lain yang tersedia:
```bash
bun run db:generate
bun run db:migrate
bun run db:seed
bun run typecheck
```
### ML Service
```bash
cd apps/ml-service
cargo run
```
Default path model adalah:
```text
../../Machine_Learning/model/model.onnx
```
Jika model berada di lokasi lain, gunakan environment variable `MODEL_PATH`:
```bash
MODEL_PATH=/path/to/model.onnx cargo run
```
**Port Configuration:**
- **Default (tanpa .env):** Service mendengarkan di `http://localhost:8000`
- **Local development (dengan .env.example):** Service mendengarkan di `http://localhost:8001`
```bash
cd apps/ml-service
source .env.example
cargo run
```
- **Docker container:** Service mendengarkan di port `8000`
Lihat `apps/ml-service/README.md` untuk detail lengkap tentang konfigurasi port dan contoh curl.
## Docker Deployment
File `docker-compose.yml` di root menyiapkan tiga service produksi:
- `web` untuk frontend
- `api` untuk backend
- `ml` untuk layanan inferensi machine learning
Konfigurasi compose menggunakan image dari GitHub Container Registry:
```text
ghcr.io/${GITHUB_REPOSITORY:-mytheclipse/zeavis-edu}/web:main
ghcr.io/${GITHUB_REPOSITORY:-mytheclipse/zeavis-edu}/api:main
ghcr.io/${GITHUB_REPOSITORY:-mytheclipse/zeavis-edu}/ml:main
```
Compose juga mengasumsikan network eksternal bernama `app-shared-net` dan routing Traefik untuk domain produksi. Service `ml` berjalan pada port `8000` di dalam container.
Contoh menjalankan compose setelah environment dan network siap:
```bash
docker compose up -d
```
## Telemetry Stack
Proyek ini menyertakan pipeline telemetry metric sebagai git submodule di `telemetry/`. Pipeline mengalirkan metrik dari seluruh service ZeaVis Edu ke ClickHouse untuk analisis dan visualisasi jangka panjang.
### Arsitektur (Production)
Di production, aplikasi dan telemetry berjalan di **VPS terpisah** dan terhubung via **Tailscale** (mesh VPN). Prometheus di VPS telemetry melakukan scrape ke service ZeaVis Edu melalui IP Tailscale masing-masing.
```mermaid
flowchart LR
subgraph VPS1["VPS — ZeaVis Edu (App)"]
W[Web / React<br/>api-zeavisedu.asepharyana.id]
A[API / Elysia<br/>:3000]
M[ML Service / Axum<br/>:8000]
end
subgraph VPS2["VPS — Telemetry Stack"]
P[Prometheus<br/>:9090]
MI[Metric Ingester<br/>:9091]
V[Vector<br/>:9001]
CH[ClickHouse<br/>:8123]
QP[Query Proxy<br/>:9092]
TUI[Telemetry UI<br/>:8181]
end
P -.->|"scrape via Tailscale IP<br/>100.x.x.a:3000/metrics"| A
P -.->|"scrape via Tailscale IP<br/>100.x.x.a:8000/metrics"| M
P -->|remote_write| MI
MI --> V
V --> CH
QP --> CH
TUI --> QP
```
Setiap service ZeaVis Edu mengekspos endpoint `/metrics` dalam format Prometheus text:
| Service | Endpoint | Port (lokal) |
|-----------------------|--------------------|--------------|
| Web (Vite dev) | `GET /metrics` | 5173 |
| API (Elysia) | `GET /metrics` | 3000 |
| ML Service (Axum) | `GET /metrics` | 8000 |
Prometheus di VPS telemetry melakukan **scrape langsung** ke API dan ML service melalui IP Tailscale mereka, bukan melalui domain publik. Konfigurasi target ada di `telemetry/prometheus/targets/zeavis-edu.json` — isi dengan IP Tailscale dari service yang dituju.
Lihat [`METRICS.md`](./METRICS.md) untuk daftar lengkap metrik yang diekspos.
### Service Telemetry
| # | Service | Peran | Port |
|---|---------|------|------|
| 1 | **Prometheus** | Metric scraping & remote_write | 9090 |
| 2 | **Metric Ingester** | Enrichment, filtering, aggregation | 9091 |
| 3 | **Vector** | Buffering, backpressure, retry | 9001 |
| 4 | **ClickHouse** | Columnar analytical storage | 8123 / 9000 |
| 5 | **Query Proxy** | Read-only SQL proxy, tenant isolation | 9092 |
| 6 | **Telemetry UI** | Vue 3 metrics dashboard | 8181 |
### Arsitektur (Local Dev)
Untuk development lokal di satu mesin, telemetry dan app bisa jalan bareng di satu Docker host. Prometheus bisa scrape service lewat Docker network yang sama.
```bash
# Setup network
docker network create app-shared-net
# Build & start telemetry (dengan network sharing)
make telemetry-up-local
```
### Menjalankan Telemetry Stack
Semua operasi telemetry dijalankan dari **root proyek** melalui Makefile:
```bash
# Build komponen telemetry (metric-ingester + telemetry-ui)
make telemetry-build
# Start semua service telemetry (mode produksi, via Tailscale)
make telemetry-up
# Start semua service telemetry (mode lokal — port langsung terbuka)
make telemetry-up-local
# Cek status kesehatan semua service
make telemetry-status
# Lihat log (semua service, atau filter dengan s=)
make telemetry-logs
make telemetry-logs s=metric-ingester
# Restart service tertentu
make telemetry-restart s=prometheus
# Kirim test metric
make telemetry-test-metric
# Stop semua service
make telemetry-down
```
Untuk development lokal:
```bash
# Setup network jika belum ada
docker network create telemetry-net
docker network create app-shared-net
# Build & start
make telemetry-build
make telemetry-up-local
# Buka dashboard di http://localhost:8181
```
### Prometheus Auto-Discovery
Prometheus menggunakan `file_sd_configs` untuk menemukan target secara dinamis. Cukup letakkan file JSON di `telemetry/prometheus/targets/` dan Prometheus akan otomatis mendeteksinya dalam 15 detik — tanpa restart.
File template sudah tersedia di [`telemetry/prometheus/targets/zeavis-edu.json`](telemetry/prometheus/targets/zeavis-edu.json). **Sebelum production, isi `__CHANGE_ME__` dengan IP Tailscale masing-masing service:**
```json
[
{ "targets": ["100.x.x.a:3000"], "labels": { "service": "zeavis-api", "component": "backend", "env": "production" } },
{ "targets": ["100.x.x.a:8000"], "labels": { "service": "zeavis-ml", "component": "inference", "env": "production" } }
]
```
> **Catatan:** Aplikasi ZeaVis Edu mengekspose port Docker-nya (`:3000`, `:8000`) langsung ke host via `docker-compose.yml`. Pastikan port-port tersebut terbuka di network Tailscale (biasanya iptables Tailscale mengizinkan koneksi ke port localhost).
### Environment Variables Telemetry
| Variable | Default | Deskripsi |
|----------|---------|-----------|
| `CLICKHOUSE_USER` | `telemetry` | User ClickHouse |
| `CLICKHOUSE_PASSWORD` | `telemetry` | Password ClickHouse |
## Workflow Machine Learning
Detail lengkap tersedia di [`Machine_Learning/README.md`](Machine_Learning/README.md). Ringkasnya:
1. Unduh `dataset_1.zip`, `dataset_2.zip`, dan `dataset_3.zip` lalu letakkan di `Machine_Learning/`.
2. Jalankan preprocessing lokal:
```bash
cd Machine_Learning
python preprocessing.py
```
3. Upload `dataset.zip` ke Google Drive.
4. Jalankan `notebook.ipynb` di Google Colab dengan GPU.
5. Download model terbaik sebagai `best_model/best_model.keras`.
6. Ekspor model produksi:
```bash
python save_model.py
```
7. Konversi TensorFlow.js via CLI:
```bash
export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
tensorflowjs_converter \
--input_format=tf_saved_model \
--output_format=tfjs_graph_model \
--signature_name=serving_default \
--saved_model_tags=serve \
model/saved_model \
model/tfjs_model
```
Output utama pipeline ML:
| Path | Kegunaan |
|---|---| |---|---|
| `Machine_Learning/dataset.zip` | Dataset siap upload ke Colab | | `DATABASE_URL` | URL koneksi PostgreSQL |
| `Machine_Learning/best_model/best_model.keras` | Model Keras hasil training | | `SESSION_SECRET` | Secret untuk session auth |
| `Machine_Learning/model/saved_model/` | TensorFlow SavedModel | | `WEB_APP_URL` | URL frontend (untuk CORS) |
| `Machine_Learning/model/model.tflite` | Model untuk mobile/TFLite | | `ML_SERVICE_URL` | URL layanan inferensi ML |
| `Machine_Learning/model/model.onnx` | Model untuk Rust ONNX Runtime |
| `Machine_Learning/model/tfjs_model/` | Model untuk TensorFlow.js |
## Artifact Lokal dan Generated Files ### Pipeline ML (Ringkasan)
Beberapa file tidak tersedia di fresh clone karena berukuran besar, dihasilkan lokal, atau berasal dari sumber eksternal: 1. Unduh 3 dataset ZIP → letakkan di `Machine_Learning/`
2. `python preprocessing.py` — gabungkan & bersihkan dataset
3. Upload `dataset.zip` ke Google Drive
4. Jalankan `notebook.ipynb` di Google Colab (GPU T4)
5. Download `best_model.keras`
6. `python save_model.py` → TFLite + SavedModel
7. Konversi ke TFJS & ONNX
- `Machine_Learning/dataset_1.zip` > 📖 **Panduan lengkap:** [`Machine_Learning/README.md`](Machine_Learning/README.md)
- `Machine_Learning/dataset_2.zip`
- `Machine_Learning/dataset_3.zip`
- `Machine_Learning/dataset/`
- `Machine_Learning/dataset.zip`
- `Machine_Learning/best_model/best_model.keras`
- `Machine_Learning/model/saved_model/`
- `Machine_Learning/model/model.tflite`
- `Machine_Learning/model/model.onnx`
- `Machine_Learning/model/tfjs_model/`
## Environment Variable Penting ### Deployment
| Variable | Digunakan oleh | Keterangan |
|---|---|---|
| `DATABASE_URL` | API | URL koneksi PostgreSQL untuk Drizzle |
| `API_PORT` | API | Port backend produksi |
| `WEB_APP_URL` | API | URL frontend untuk konfigurasi CORS/integrasi |
| `ML_SERVICE_URL` | API | URL layanan ML |
| `MODEL_PATH` | ML Service | Lokasi file model ONNX, default `../../Machine_Learning/model/model.onnx` |
| `MODEL_INPUT_SIZE` | ML Service | Ukuran input model, default produksi `224` |
## Troubleshooting
### `bun run dev` gagal karena dependency belum tersedia
Jalankan ulang instalasi dari root repository:
```bash ```bash
bun install docker compose up -d # App services
make telemetry-up # Telemetry stack
``` ```
### API membutuhkan database > 📖 **Panduan infrastruktur:** [`infra/README.md`](infra/README.md)
Pastikan `DATABASE_URL` tersedia di `.env` root dan PostgreSQL dapat diakses oleh aplikasi. ---
### ML service gagal memuat model ## 📚 Dokumentasi
Pastikan file model tersedia di path default: | Dokumen | Isi |
|---|---|
| [`Machine_Learning/README.md`](Machine_Learning/README.md) | Pipeline ML lengkap — preprocessing, training Colab, ekspor TFLite/TFJS/ONNX |
| [`apps/ml-service/README.md`](apps/ml-service/README.md) | ML Inference Service — setup, endpoint API, konfigurasi |
| [`infra/README.md`](infra/README.md) | Arsitektur multi-VPS — diagram, GitHub Secrets, port, metrics flow |
| [`METRICS.md`](METRICS.md) | Daftar lengkap metrik Prometheus |
| `telemetry/` (submodule) | Source code telemetry stack |
```text ---
Machine_Learning/model/model.onnx
```
Atau set path khusus: ## 🔧 Troubleshooting
```bash | Masalah | Solusi |
MODEL_PATH=/path/to/model.onnx cargo run |---|---|
``` | `bun install` gagal | `bun --version` — pastikan ≥ 1.x |
| API perlu database | Isi `DATABASE_URL` di root `.env` |
| ML service gagal muat model | `ls Machine_Learning/model/model.onnx` — jalankan pipeline ML jika belum ada |
| Docker Compose gagal | `docker network create app-shared-net` |
| Konversi TFJS gagal | `export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python` |
### Docker Compose gagal karena network tidak ditemukan ---
`docker-compose.yml` menggunakan network eksternal `app-shared-net`. Buat network tersebut jika belum ada: ## 📖 Daftar Pustaka
```bash 1. Prayogi, A. et al. *"Klasifikasi Penyakit Daun Jagung Menggunakan CNN"* — [SISTEMATIS](https://ejournal.rizaniamedia.com/index.php/sistematis/article/view/87/49)
docker network create app-shared-net 2. Nugroho, A. et al. *"Deteksi Penyakit Daun Jagung dengan Deep Learning"* — [MIND Journal](https://ejurnal.itenas.ac.id/index.php/mindjournal/article/view/14032/4209)
``` 3. Ramadhan, F. et al. *"Identifikasi Penyakit Jagung Berbasis Citra Digital"* — [Informa](https://www.informa.poltekindonusa.ac.id/index.php/informa/article/view/199/170)
4. Corteva Agriscience. *"Kenali Ragam Jenis Penyakit Jagung dan Cara Mengatasinya"* — [corteva.com](https://www.corteva.com/id/berita/Kenali-Ragam-Jenis-Penyakit-Jagung-dan-Cara-Mengatasinya.html)
### Konversi TensorFlow.js gagal karena konflik protobuf ---
Jalankan konversi melalui CLI dan set environment variable berikut: <p align="center">
<sub>
```bash Capstone Project • Pijak × IBM SkillsBuild • AI for Smart Education<br>
export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python © 2026 ZeaVis Edu Team
``` </sub>
</p>
## Pengembangan
Alur umum pengembangan:
1. Install dependency dengan `bun install`.
2. Jalankan service yang dibutuhkan secara lokal.
3. Jalankan `bun run typecheck` sebelum membuat commit.
4. Jalankan `bun run build` untuk memverifikasi build produksi.
5. Untuk perubahan ML, ikuti dokumentasi detail di `Machine_Learning/README.md`.
6. Untuk perubahan ML service, cek juga `apps/ml-service/README.md`.
## Dokumentasi Terkait
- [`Machine_Learning/README.md`](Machine_Learning/README.md) — panduan lengkap dataset, training, dan ekspor model.
- [`apps/ml-service/README.md`](apps/ml-service/README.md) — panduan menjalankan dan memverifikasi layanan inferensi ML.
+62 -17
View File
@@ -55,9 +55,6 @@ async function exchangeGoogleCode(code: string): Promise<GoogleTokenResponse> {
} }
function decodeGoogleIdToken(idToken: string): GoogleIdPayload { function decodeGoogleIdToken(idToken: string): GoogleIdPayload {
// JWT: header.payload.signature — we only need the payload
// Google's id_token is verified via the token endpoint (direct server-to-server),
// so we can safely decode without verifying the signature here.
const parts = idToken.split('.'); const parts = idToken.split('.');
if (parts.length !== 3) { if (parts.length !== 3) {
throw new Error('Invalid id_token format'); throw new Error('Invalid id_token format');
@@ -66,6 +63,41 @@ function decodeGoogleIdToken(idToken: string): GoogleIdPayload {
return JSON.parse(payload); return JSON.parse(payload);
} }
/**
* Render a page for the Android system browser that redirects back to the
* Tauri app via a custom scheme (zeavisedu://). The app's AndroidManifest
* must register an intent filter for this scheme.
*/
function renderTauriDeepLinkPage(targetUrl: string): Response {
// Rewrite https://... to zeavisedu://... for the custom scheme
const deepLink = targetUrl.replace(/^https?:\/\//, 'zeavisedu://');
const html = `<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Kembali ke ZeaVis Edu</title></head>
<body style="font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f0fdf4">
<div style="text-align:center;padding:2rem">
<p style="color:#166534;font-size:1.1rem;margin-bottom:1.5rem">Login berhasil!<br>Kembali ke aplikasi...</p>
<a href="${deepLink.replace(/"/g, '&quot;')}" style="display:inline-block;background:#16a34a;color:white;padding:0.75rem 2rem;border-radius:0.5rem;text-decoration:none;font-weight:600;font-size:1rem">Buka ZeaVis Edu</a>
<p style="color:#6b7280;font-size:0.8rem;margin-top:1rem">Jika tombol tidak berfungsi, salin URL ini:<br><code style="word-break:break-all;font-size:0.75rem">${deepLink.replace(/</g, '&lt;')}</code></p>
</div>
<script>window.location.href=${JSON.stringify(deepLink)};</script>
</body></html>`;
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html;charset=utf-8' },
});
}
function resolvePlatform(stateRaw: string | undefined): string {
try {
if (stateRaw) {
const parsed = JSON.parse(Buffer.from(stateRaw, 'base64url').toString('utf-8'));
return parsed.platform ?? 'web';
}
} catch { /* ignore */ }
return 'web';
}
function normalizeEmail(email: unknown) { function normalizeEmail(email: unknown) {
return typeof email === 'string' ? email.trim().toLowerCase() : ''; return typeof email === 'string' ? email.trim().toLowerCase() : '';
} }
@@ -171,12 +203,15 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
set.headers['Set-Cookie'] = clearSessionCookie(request.headers); set.headers['Set-Cookie'] = clearSessionCookie(request.headers);
return { ok: true }; return { ok: true };
}) })
.get('/google', ({ set }) => { .get('/google', ({ query, set }) => {
if (!env.googleOAuthEnabled) { if (!env.googleOAuthEnabled) {
set.status = 404; set.status = 404;
return { error: 'Google OAuth is not configured' }; return { error: 'Google OAuth is not configured' };
} }
const platform = (query as Record<string, string>).platform ?? 'web';
const state = Buffer.from(JSON.stringify({ platform })).toString('base64url');
const params = new URLSearchParams({ const params = new URLSearchParams({
client_id: env.googleClientId!, client_id: env.googleClientId!,
redirect_uri: env.googleRedirectUri!, redirect_uri: env.googleRedirectUri!,
@@ -184,6 +219,7 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
scope: 'openid email profile', scope: 'openid email profile',
access_type: 'offline', access_type: 'offline',
prompt: 'select_account', prompt: 'select_account',
state,
}); });
set.status = 302; set.status = 302;
@@ -195,13 +231,20 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
return { error: 'Google OAuth is not configured' }; return { error: 'Google OAuth is not configured' };
} }
const code = (query as Record<string, string>).code; const q = query as Record<string, string>;
const error = (query as Record<string, string>).error; const code = q.code;
const error = q.error;
const platform = resolvePlatform(q.state);
// User denied or Google returned an error // User denied or Google returned an error
const makeErrorUrl = (msg: string) =>
`${env.webAppUrl}/login?error=${encodeURIComponent(msg)}`;
if (error || !code) { if (error || !code) {
const url = makeErrorUrl(error ?? 'missing_code');
if (platform === 'tauri') return renderTauriDeepLinkPage(url);
set.status = 302; set.status = 302;
set.headers['Location'] = `${env.webAppUrl}/login?error=${encodeURIComponent(error ?? 'missing_code')}`; set.headers['Location'] = url;
return; return;
} }
@@ -212,15 +255,19 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
idPayload = decodeGoogleIdToken(tokens.id_token); idPayload = decodeGoogleIdToken(tokens.id_token);
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : 'Google auth failed'; const msg = err instanceof Error ? err.message : 'Google auth failed';
const url = makeErrorUrl(msg);
if (platform === 'tauri') return renderTauriDeepLinkPage(url);
set.status = 302; set.status = 302;
set.headers['Location'] = `${env.webAppUrl}/login?error=${encodeURIComponent(msg)}`; set.headers['Location'] = url;
return; return;
} }
// Validate email // Validate email
if (!idPayload.email_verified || !idPayload.email) { if (!idPayload.email_verified || !idPayload.email) {
const url = makeErrorUrl('Email not verified by Google');
if (platform === 'tauri') return renderTauriDeepLinkPage(url);
set.status = 302; set.status = 302;
set.headers['Location'] = `${env.webAppUrl}/login?error=${encodeURIComponent('Email not verified by Google')}`; set.headers['Location'] = url;
return; return;
} }
@@ -231,19 +278,15 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
try { try {
const db = createDbClient(); const db = createDbClient();
// 1. Try to find user by googleId
let user = await db.select().from(users).where(eq(users.googleId, googleId)).limit(1).then(r => r[0] ?? null); let user = await db.select().from(users).where(eq(users.googleId, googleId)).limit(1).then(r => r[0] ?? null);
// 2. If not found, try by email (link existing account)
if (!user) { if (!user) {
user = await db.select().from(users).where(eq(users.email, email)).limit(1).then(r => r[0] ?? null); user = await db.select().from(users).where(eq(users.email, email)).limit(1).then(r => r[0] ?? null);
if (user) { if (user) {
// Link googleId to existing account
await db.update(users).set({ googleId }).where(eq(users.id, user.id)); await db.update(users).set({ googleId }).where(eq(users.id, user.id));
} }
} }
// 3. Create new user if nothing matched
if (!user) { if (!user) {
const inserted = await db const inserted = await db
.insert(users) .insert(users)
@@ -253,17 +296,19 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
authCounter.labels('register', 'true').inc(); authCounter.labels('register', 'true').inc();
} }
// Create session
const token = await createSession(user.id); const token = await createSession(user.id);
set.headers['Set-Cookie'] = createSessionCookie(token, request.headers); set.headers['Set-Cookie'] = createSessionCookie(token, request.headers);
authCounter.labels('login', 'true').inc(); authCounter.labels('login', 'true').inc();
// Redirect to web app with token in URL for localStorage fallback const successUrl = `${env.webAppUrl}/login?token=${encodeURIComponent(token)}`;
if (platform === 'tauri') return renderTauriDeepLinkPage(successUrl);
set.status = 302; set.status = 302;
set.headers['Location'] = `${env.webAppUrl}/login?token=${encodeURIComponent(token)}`; set.headers['Location'] = successUrl;
} catch (err) { } catch (err) {
const url = makeErrorUrl('Database unavailable');
if (platform === 'tauri') return renderTauriDeepLinkPage(url);
set.status = 302; set.status = 302;
set.headers['Location'] = `${env.webAppUrl}/login?error=${encodeURIComponent('Database unavailable')}`; set.headers['Location'] = url;
} }
}); });
+97 -163
View File
@@ -1,8 +1,25 @@
# ML Service — Rust Axum ONNX Runtime # ML Inference Service — ZeaVis Edu
Layanan inferensi machine learning berbasis Rust dengan Axum web framework dan ONNX Runtime untuk klasifikasi penyakit daun jagung. Service ini menyediakan endpoint HTTP untuk prediksi real-time dengan performa tinggi dan konsumsi resource minimal. > Layanan inferensi machine learning berbasis Rust/Axum + ONNX Runtime untuk klasifikasi penyakit daun jagung.
## Fitur ← [Kembali ke README utama](../../README.md)
---
## Daftar Isi
1. [Fitur](#1-fitur)
2. [Prasyarat & Instalasi](#2-prasyarat--instalasi)
3. [Menjalankan Service](#3-menjalankan-service)
4. [Environment Variables](#4-environment-variables)
5. [Endpoint API](#5-endpoint-api)
6. [Verifikasi & Testing](#6-verifikasi--testing)
7. [Docker Deployment](#7-docker-deployment)
8. [Troubleshooting](#8-troubleshooting)
---
## 1. Fitur
- **Framework:** Axum (async Rust web framework) - **Framework:** Axum (async Rust web framework)
- **Runtime Inferensi:** ONNX Runtime untuk kompatibilitas lintas platform - **Runtime Inferensi:** ONNX Runtime untuk kompatibilitas lintas platform
@@ -10,15 +27,13 @@ Layanan inferensi machine learning berbasis Rust dengan Axum web framework dan O
- **Endpoint:** Health check, metadata, dan prediksi gambar - **Endpoint:** Health check, metadata, dan prediksi gambar
- **Multipart Upload:** Dukungan upload gambar langsung via HTTP POST - **Multipart Upload:** Dukungan upload gambar langsung via HTTP POST
## Prasyarat ---
## 2. Prasyarat & Instalasi
- Rust 1.70+ dan Cargo - Rust 1.70+ dan Cargo
- Model ONNX di `../../Machine_Learning/model/model.onnx` (atau path custom via `MODEL_PATH`) - Model ONNX di `../../Machine_Learning/model/model.onnx` (atau path custom via `MODEL_PATH`)
## Instalasi & Setup
### Instalasi Dependensi
Dependensi Rust sudah terdaftar di `Cargo.toml`. Cargo akan mengunduh dan mengkompilasi otomatis saat pertama kali build. Dependensi Rust sudah terdaftar di `Cargo.toml`. Cargo akan mengunduh dan mengkompilasi otomatis saat pertama kali build.
```bash ```bash
@@ -27,75 +42,58 @@ cargo build
Output build lokal berada di `target/` dan direktori tersebut diabaikan oleh Git. Output build lokal berada di `target/` dan direktori tersebut diabaikan oleh Git.
## Menjalankan Service Lokal ---
## 3. Menjalankan Service
Semua perintah di bawah dijalankan dari direktori `apps/ml-service`. Semua perintah di bawah dijalankan dari direktori `apps/ml-service`.
### Opsi 1: Default (Port 8000, Model dari Machine_Learning/) ### Opsi 1: Default (Port 8000)
```bash ```bash
cd apps/ml-service
cargo run cargo run
``` ```
Service akan mencari model di path default dan mendengarkan di `http://localhost:8000`: Service akan mencari model di path default:
``` ```
../../Machine_Learning/model/model.onnx ../../Machine_Learning/model/model.onnx
``` ```
### Opsi 2: Local Development dengan .env.example (Port 8001) ### Opsi 2: Local Development dengan .env.example (Port 8001)
Untuk development lokal dengan port 8001 (sesuai `.env.example`):
```bash ```bash
cd apps/ml-service
source .env.example source .env.example
cargo run cargo run
``` ```
Service akan mendengarkan di `http://localhost:8001` karena `ML_SERVICE_PORT=8001` di `.env.example`. ### Opsi 3: Custom Model Path & Port
### Opsi 3: Custom Model Path
Jika model berada di lokasi lain, gunakan environment variable `MODEL_PATH`:
```bash ```bash
cd apps/ml-service
MODEL_PATH=/path/to/model.onnx cargo run
```
Atau kombinasikan dengan port custom:
```bash
cd apps/ml-service
ML_SERVICE_PORT=9000 MODEL_PATH=/path/to/model.onnx cargo run ML_SERVICE_PORT=9000 MODEL_PATH=/path/to/model.onnx cargo run
``` ```
## Environment Variables ---
## 4. Environment Variables
| Variable | Default | Keterangan | | Variable | Default | Keterangan |
|---|---|---| |---|---|---|
| `ML_SERVICE_HOST` | `0.0.0.0` | Bind address | | `ML_SERVICE_HOST` | `0.0.0.0` | Bind address |
| `ML_SERVICE_PORT` | `8000` | Bind port (override untuk local dev dengan `.env.example`) | | `ML_SERVICE_PORT` | `8000` | Bind port |
| `MODEL_PATH` | `../../Machine_Learning/model/model.onnx` | Path ke file model ONNX | | `MODEL_PATH` | `../../Machine_Learning/model/model.onnx` | Path ke file model ONNX |
| `MODEL_INPUT_SIZE` | `224` | Ukuran input gambar (224x224 untuk EfficientNetV2B0) | | `MODEL_INPUT_SIZE` | `224` | Ukuran input gambar (224×224 untuk EfficientNetV2B0) |
| `RUST_LOG` | `info` | Level logging (debug, info, warn, error) | | `RUST_LOG` | `info` | Level logging (debug, info, warn, error) |
## Endpoint API ---
### 1. Health Check ## 5. Endpoint API
### Health Check
**Default (port 8000):**
```bash ```bash
curl http://localhost:8000/health curl http://localhost:8000/health
``` ```
**Local dev dengan .env.example (port 8001):**
```bash
curl http://localhost:8001/health
```
**Response:**
```json ```json
{ {
"status": "ok", "status": "ok",
@@ -103,19 +101,12 @@ curl http://localhost:8001/health
} }
``` ```
### 2. Metadata ### Metadata
**Default (port 8000):**
```bash ```bash
curl http://localhost:8000/metadata curl http://localhost:8000/metadata
``` ```
**Local dev dengan .env.example (port 8001):**
```bash
curl http://localhost:8001/metadata
```
**Response:**
```json ```json
{ {
"service_name": "zeavis-ml-service", "service_name": "zeavis-ml-service",
@@ -123,32 +114,19 @@ curl http://localhost:8001/metadata
"model_path": "../../Machine_Learning/model/model.onnx", "model_path": "../../Machine_Learning/model/model.onnx",
"model_loaded": true, "model_loaded": true,
"input_size": 224, "input_size": 224,
"labels": [ "labels": ["Bercak Daun", "Daun Sehat", "Karat Daun", "Hawar Daun"]
"Bercak Daun",
"Daun Sehat",
"Karat Daun",
"Hawar Daun"
]
} }
``` ```
### 3. Prediksi ### Prediksi
Upload gambar daun jagung untuk klasifikasi: Upload gambar daun jagung untuk klasifikasi:
**Default (port 8000):**
```bash ```bash
curl -X POST http://localhost:8000/predict \ curl -X POST http://localhost:8000/predict \
-F "file=@/path/to/corn-leaf.jpg" -F "file=@/path/to/corn-leaf.jpg"
``` ```
**Local dev dengan .env.example (port 8001):**
```bash
curl -X POST http://localhost:8001/predict \
-F "file=@/path/to/corn-leaf.jpg"
```
**Response:**
```json ```json
{ {
"label": "Daun Sehat", "label": "Daun Sehat",
@@ -162,120 +140,45 @@ curl -X POST http://localhost:8001/predict \
} }
``` ```
## Verifikasi & Testing ---
## 6. Verifikasi & Testing
### Build Produksi ### Build Produksi
```bash ```bash
cargo build --release cargo build --release
# Binary di target/release/zeavis-ml-service
``` ```
Output binary akan tersedia di `target/release/zeavis-ml-service`.
### Menjalankan Tests ### Menjalankan Tests
```bash ```bash
cargo test cargo test
``` ```
Tests mencakup validasi loading model, preprocessing gambar, dan output prediksi. ### Verifikasi Manual (default port 8000)
### Verifikasi Manual
#### Dengan default port 8000:
1. Jalankan service:
```bash
cargo run
```
2. Di terminal lain, test health endpoint:
```bash
curl http://localhost:8000/health
```
3. Test metadata:
```bash
curl http://localhost:8000/metadata
```
4. Test prediksi dengan gambar sample:
```bash
curl -X POST http://localhost:8000/predict \
-F "file=@../../Machine_Learning/dataset/Daun\ Sehat/sample.jpg"
```
#### Dengan local dev port 8001 (.env.example):
1. Jalankan service dengan .env.example:
```bash
source .env.example
cargo run
```
2. Di terminal lain, test health endpoint:
```bash
curl http://localhost:8001/health
```
3. Test metadata:
```bash
curl http://localhost:8001/metadata
```
4. Test prediksi dengan gambar sample:
```bash
curl -X POST http://localhost:8001/predict \
-F "file=@../../Machine_Learning/dataset/Daun\ Sehat/sample.jpg"
```
## Troubleshooting
### Model tidak ditemukan
**Error:** `Failed to load model: No such file or directory`
**Solusi:** Pastikan file model tersedia di path yang benar:
```bash
ls -la ../../Machine_Learning/model/model.onnx
```
Atau set path custom:
```bash
MODEL_PATH=/absolute/path/to/model.onnx cargo run
```
### Port sudah digunakan
**Error:** `Address already in use`
**Solusi:** Service menggunakan port 8000 secara default. Jika port sudah digunakan, ubah dengan environment variable:
```bash ```bash
ML_SERVICE_PORT=9000 cargo run # 1. Start service
cargo run
# 2. Health check
curl http://localhost:8000/health
# 3. Metadata
curl http://localhost:8000/metadata
# 4. Prediksi
curl -X POST http://localhost:8000/predict \
-F "file=@../../Machine_Learning/dataset/Daun\ Sehat/sample.jpg"
``` ```
Atau jika menggunakan `.env.example` (port 8001), pastikan tidak ada service lain di port tersebut: ---
```bash ## 7. Docker Deployment
lsof -i :8001
```
### ONNX Runtime tidak kompatibel Service dapat di-deploy via Docker. Build dari root repository karena Dockerfile menyalin source service dan artifact ONNX dari beberapa direktori repo.
**Error:** `ONNX Runtime initialization failed`
**Solusi:** Pastikan ONNX Runtime binary kompatibel dengan sistem operasi. Cargo akan mengunduh binary yang sesuai otomatis. Jika masalah persisten, coba rebuild:
```bash
cargo clean
cargo build
```
## Deployment
### Docker
Service dapat di-deploy via Docker. Jalankan build dari root repository karena Dockerfile menyalin source service dan artifact ONNX dari beberapa direktori repo.
```bash ```bash
docker build -f apps/ml-service/Dockerfile -t zeavis-ml-service . docker build -f apps/ml-service/Dockerfile -t zeavis-ml-service .
@@ -284,11 +187,42 @@ docker run -p 8000:8000 zeavis-ml-service
Pastikan `Machine_Learning/model/model.onnx` sudah dibuat sebelum build image. Pastikan `Machine_Learning/model/model.onnx` sudah dibuat sebelum build image.
### Docker Compose ---
Lihat `docker-compose.yml` di root repository untuk deployment lengkap dengan web, API, dan ML service. ## 8. Troubleshooting
## Dokumentasi Terkait ### Model tidak ditemukan
- [`Machine_Learning/README.md`](../../Machine_Learning/README.md) — Panduan training dan ekspor model ONNX **Error:** `Failed to load model: No such file or directory`
- [`README.md`](../../README.md) — Dokumentasi proyek utama
**Solusi:**
```bash
ls -la ../../Machine_Learning/model/model.onnx
# Atau set path custom:
MODEL_PATH=/absolute/path/to/model.onnx cargo run
```
### Port sudah digunakan
**Error:** `Address already in use`
**Solusi:**
```bash
ML_SERVICE_PORT=9000 cargo run
# Cek port yang digunakan:
lsof -i :8000
```
### ONNX Runtime tidak kompatibel
**Error:** `ONNX Runtime initialization failed`
**Solusi:** Pastikan binary ONNX Runtime kompatibel dengan sistem operasi. Jika masalah persisten:
```bash
cargo clean
cargo build
```
---
← [Kembali ke README utama](../../README.md) &bull; [Pipeline ML →](../../Machine_Learning/README.md) &bull; [Infra →](../../infra/README.md)
+2
View File
@@ -12,5 +12,7 @@ tauri-build = { version = "2", features = [] }
[dependencies] [dependencies]
tauri = { version = "2", default-features = false, features = ["wry", "common-controls-v6", "dynamic-acl", "x11", "dbus", "custom-protocol"] } tauri = { version = "2", default-features = false, features = ["wry", "common-controls-v6", "dynamic-acl", "x11", "dbus", "custom-protocol"] }
tauri-plugin-opener = "2"
tauri-plugin-deep-link = "2"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
+4 -1
View File
@@ -3,6 +3,9 @@
"description": "Capability for the main window", "description": "Capability for the main window",
"windows": ["main"], "windows": ["main"],
"permissions": [ "permissions": [
"core:default" "core:default",
"opener:default",
"opener:allow-open-url",
"deep-link:default"
] ]
} }
+32 -7
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Patches the generated AndroidManifest.xml to add CAMERA permission. # Patches the generated AndroidManifest.xml with:
# 1. CAMERA permission
# 2. Deep link intent filter (zeavisedu:// scheme) for Google OAuth return
# Run after `tauri android init` to apply. # Run after `tauri android init` to apply.
set -euo pipefail set -euo pipefail
@@ -10,11 +12,34 @@ if [ ! -f "$MANIFEST" ]; then
exit 1 exit 1
fi fi
if grep -q 'android.permission.CAMERA' "$MANIFEST"; then # ── CAMERA permission ──────────────────────────────────────────────────
echo "CAMERA permission already present in AndroidManifest.xml"
exit 0 if ! grep -q 'android.permission.CAMERA' "$MANIFEST"; then
echo "Adding CAMERA permission to AndroidManifest.xml..."
sed -i 's|<uses-permission android:name="android.permission.INTERNET" />|<uses-permission android:name="android.permission.INTERNET" />\n <uses-permission android:name="android.permission.CAMERA" />\n <uses-feature android:name="android.hardware.camera" android:required="false" />\n <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />|' "$MANIFEST"
else
echo "CAMERA permission already present."
fi fi
echo "Adding CAMERA permission to AndroidManifest.xml..." # ── Deep link intent filter ────────────────────────────────────────────
sed -i 's|<uses-permission android:name="android.permission.INTERNET" />|<uses-permission android:name="android.permission.INTERNET" />\n <uses-permission android:name="android.permission.CAMERA" />\n <uses-feature android:name="android.hardware.camera" android:required="false" />\n <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />|' "$MANIFEST" # Allows the app to receive zeavisedu:// scheme URLs from the system browser
echo "Done." # (used after Google OAuth completes in external browser on Android)
DEEP_LINK_FILTER='<!-- Deep link for Google OAuth return from system browser -->\
<intent-filter android:autoVerify="true">\
<action android:name="android.intent.action.VIEW" />\
<category android:name="android.intent.category.DEFAULT" />\
<category android:name="android.intent.category.BROWSABLE" />\
<data android:scheme="zeavisedu" />\
</intent-filter>'
if grep -q 'android:scheme="zeavisedu"' "$MANIFEST"; then
echo "Deep link intent filter already present."
else
echo "Adding deep link intent filter to AndroidManifest.xml..."
# Insert before the closing </activity> tag of MainActivity
sed -i "s|</activity>|${DEEP_LINK_FILTER}\n </activity>|" "$MANIFEST"
echo "Deep link intent filter added."
fi
echo "AndroidManifest patched successfully."
+2
View File
@@ -1,6 +1,8 @@
#[cfg_attr(mobile, tauri::mobile_entry_point)] #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_deep_link::init())
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }
+6
View File
@@ -21,6 +21,7 @@ import { MainLayout } from "@/components/layout/main-layout";
import { useEffect } from "react"; import { useEffect } from "react";
import { useAuthStore } from "@/store/auth-store"; import { useAuthStore } from "@/store/auth-store";
import { apiClient } from "@/lib/api-client"; import { apiClient } from "@/lib/api-client";
import { setupDeepLinkHandler } from "@/lib/tauri";
function LogoutProses() { function LogoutProses() {
const setUser = useAuthStore((state) => state.setUser); const setUser = useAuthStore((state) => state.setUser);
@@ -161,6 +162,11 @@ function GlobalErrorTracker() {
} }
export function App() { export function App() {
// Register deep link handler for Android OAuth return
useEffect(() => {
setupDeepLinkHandler();
}, []);
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<AuthInitializer /> <AuthInitializer />
+21 -12
View File
@@ -1,9 +1,10 @@
import { FormEvent, useState } from 'react'; import { FormEvent, useState, useCallback } from 'react';
import { Eye, EyeOff } from 'lucide-react'; import { Eye, EyeOff } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { isTauri, openUrl } from '@/lib/tauri';
type AuthFormProps = { type AuthFormProps = {
mode: 'login' | 'register'; mode: 'login' | 'register';
@@ -25,6 +26,16 @@ export function AuthForm({ mode, isSubmitting, error, googleOAuthEnabled, onSubm
await onSubmit({ name, email, password }); await onSubmit({ name, email, password });
} }
const handleGoogleLogin = useCallback(async (e: React.MouseEvent) => {
e.preventDefault();
const platform = isTauri() ? 'tauri' : 'web';
// Use API base URL, not window.location.origin — on Tauri Android
// the origin is http://tauri.localhost which is not the API server.
const apiBase = import.meta.env.VITE_API_BASE_URL || window.location.origin;
const googleUrl = `${apiBase}/api/v1/auth/google?platform=${platform}`;
await openUrl(googleUrl);
}, []);
return ( return (
<Card className="mx-auto w-full max-w-md"> <Card className="mx-auto w-full max-w-md">
<CardHeader> <CardHeader>
@@ -75,17 +86,15 @@ export function AuthForm({ mode, isSubmitting, error, googleOAuthEnabled, onSubm
</Button> </Button>
</form> </form>
{googleOAuthEnabled && ( {googleOAuthEnabled && (
<Button className="mt-3 w-full flex items-center justify-center gap-2.5" variant="outline" asChild> <Button className="mt-3 w-full flex items-center justify-center gap-2.5" variant="outline" onClick={handleGoogleLogin} type="button">
<a href="/api/v1/auth/google"> <svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true">
<svg viewBox="0 0 24 24" className="h-5 w-5" aria-hidden="true"> <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" />
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" /> <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" /> <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" /> <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" /> <path fill="none" d="M1 1h22v22H1z" />
<path fill="none" d="M1 1h22v22H1z" /> </svg>
</svg> Masuk dengan Google
Masuk dengan Google
</a>
</Button> </Button>
)} )}
</CardContent> </CardContent>
+55
View File
@@ -0,0 +1,55 @@
/**
* Lightweight Tauri environment detection and utilities.
* Avoids importing @tauri-apps/api at module level so the web build
* doesn't bundle Tauri internals.
*/
let _isTauri: boolean | null = null;
export function isTauri(): boolean {
if (_isTauri !== null) return _isTauri;
_isTauri =
typeof window !== 'undefined' &&
'__TAURI_INTERNALS__' in window;
return _isTauri;
}
export async function openUrl(url: string): Promise<void> {
if (!isTauri()) {
window.location.href = url;
return;
}
const { openUrl: tauriOpenUrl } = await import('@tauri-apps/plugin-opener');
await tauriOpenUrl(url);
}
/**
* 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://zeavisedu.asepharyana.my.id/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;
}
} catch {
// If URL parsing fails, try to extract everything after the scheme
const match = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
if (match?.[1]) {
window.location.href = match[1];
}
}
}
});
}
+10
View File
@@ -4,6 +4,10 @@
"workspaces": { "workspaces": {
"": { "": {
"name": "zeavis-edu", "name": "zeavis-edu",
"dependencies": {
"@tauri-apps/plugin-deep-link": "2.4.9",
"@tauri-apps/plugin-opener": "2.5.4",
},
"devDependencies": { "devDependencies": {
"@moonrepo/cli": "^2.2.5", "@moonrepo/cli": "^2.2.5",
"typescript": "^6.0.3", "typescript": "^6.0.3",
@@ -341,6 +345,8 @@
"@tanstack/react-query": ["@tanstack/react-query@5.101.0", "", { "dependencies": { "@tanstack/query-core": "5.101.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg=="], "@tanstack/react-query": ["@tanstack/react-query@5.101.0", "", { "dependencies": { "@tanstack/query-core": "5.101.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg=="],
"@tauri-apps/api": ["@tauri-apps/api@2.11.0", "", {}, "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA=="],
"@tauri-apps/cli": ["@tauri-apps/cli@2.11.2", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.2", "@tauri-apps/cli-darwin-x64": "2.11.2", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", "@tauri-apps/cli-linux-arm64-musl": "2.11.2", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-musl": "2.11.2", "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", "@tauri-apps/cli-win32-x64-msvc": "2.11.2" }, "bin": { "tauri": "tauri.js" } }, "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw=="], "@tauri-apps/cli": ["@tauri-apps/cli@2.11.2", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.2", "@tauri-apps/cli-darwin-x64": "2.11.2", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", "@tauri-apps/cli-linux-arm64-musl": "2.11.2", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-musl": "2.11.2", "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", "@tauri-apps/cli-win32-x64-msvc": "2.11.2" }, "bin": { "tauri": "tauri.js" } }, "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw=="],
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.11.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w=="], "@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.11.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w=="],
@@ -365,6 +371,10 @@
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.2", "", { "os": "win32", "cpu": "x64" }, "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA=="], "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.2", "", { "os": "win32", "cpu": "x64" }, "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA=="],
"@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.9", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-u0SKOUHnJ1wqeqXsDFq2+kASCBj9xxbG0g9XZWPy9SOmU4wXtp6b/wiYpm6oH6/5fBTQsLqnLhIvqLBRpgHJlA=="],
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ=="],
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
+80 -33
View File
@@ -1,6 +1,25 @@
# Infra — ZeaVis Edu Multi-VPS Deployment # Infrastruktur — ZeaVis Edu
## Arsitektur > Arsitektur multi-VPS untuk deployment produksi ZeaVis Edu dengan Tailscale mesh VPN dan observabilitas penuh.
← [Kembali ke README utama](../README.md)
---
## Daftar Isi
1. [Arsitektur](#1-arsitektur)
2. [Prasyarat GitHub Secrets](#2-prasyarat-github-secrets)
3. [Setup VPS](#3-setup-vps)
4. [Port yang Dibuka](#4-port-yang-dibuka)
5. [Metrics Flow](#5-metrics-flow)
6. [Perintah Penting](#6-perintah-penting)
---
## 1. Arsitektur
ZeaVis Edu berjalan di **dua VPS terpisah** yang terhubung melalui **Tailscale** mesh VPN:
``` ```
┌─────────────────────────────────────────────┐ ┌──────────────────────────────────────────────┐ ┌─────────────────────────────────────────────┐ ┌──────────────────────────────────────────────┐
@@ -44,32 +63,41 @@
└─────────────────────────────────────────────┘ └──────────────────────────────────────────────┘ └─────────────────────────────────────────────┘ └──────────────────────────────────────────────┘
``` ```
## Prerequisites | VPS | Hostname | OS | Peran |
|---|---|---|---|
| **App VPS** | `imrnes` | Arch Linux | Web (:80), API (:3000), ML Service (:8000) |
| **Telemetry VPS** | `orange` | Ubuntu | Prometheus, ClickHouse, Telemetry UI |
### GitHub Secrets (untuk CI/CD) ---
**App VPS deploy (`.github/workflows/deploy.yml`):** ## 2. Prasyarat GitHub Secrets
| Secret | Value |
|--------|-------| ### App VPS — `.github/workflows/deploy.yml`
| Secret | Keterangan |
|---|---|
| `VPS_HOST` | `100.108.1.124` (imrnes) | | `VPS_HOST` | `100.108.1.124` (imrnes) |
| `VPS_USER` | `mytheclipse` | | `VPS_USER` | `mytheclipse` |
| `VPS_SSH_KEY` | Private SSH key for imrnes | | `VPS_SSH_KEY` | Private SSH key untuk imrnes |
| `VPS_PORT` | `22` | | `VPS_PORT` | `22` |
| `DATABASE_URL` | PostgreSQL connection string | | `DATABASE_URL` | PostgreSQL connection string |
| `SESSION_SECRET` | Random session secret | | `SESSION_SECRET` | Random session secret |
**Telemetry VPS deploy (`.github/workflows/telemetry-ci-cd.yml`):** ### Telemetry VPS `.github/workflows/telemetry-ci-cd.yml`
| Secret | Value |
|--------|-------| | Secret | Keterangan |
|---|---|
| `TELEMETRY_VPS_HOST` | `100.96.248.86` (orange) | | `TELEMETRY_VPS_HOST` | `100.96.248.86` (orange) |
| `TELEMETRY_VPS_USER` | SSH username for orange | | `TELEMETRY_VPS_USER` | SSH username |
| `TELEMETRY_VPS_SSH_KEY` | Private SSH key for orange | | `TELEMETRY_VPS_SSH_KEY` | Private SSH key |
| `TELEMETRY_VPS_PORT` | `22` | | `TELEMETRY_VPS_PORT` | `22` |
| `GHCR_PAT` | GitHub PAT with `write:packages` + `read:packages` | | `GHCR_PAT` | GitHub PAT dengan `write:packages` + `read:packages` |
### VPS Setup ---
#### 1. App VPS (imrnes — 100.108.1.124) ## 3. Setup VPS
### App VPS (imrnes — 100.108.1.124)
```bash ```bash
# Create Docker network # Create Docker network
@@ -79,33 +107,35 @@ docker network create telemetry-net
# ZeaVis Edu apps deploy automatically via GitHub Actions # ZeaVis Edu apps deploy automatically via GitHub Actions
``` ```
#### 2. Telemetry VPS (orange — 100.96.248.86) ### Telemetry VPS (orange — 100.96.248.86)
Deploy via GitHub Actions workflow `.github/workflows/telemetry-ci-cd.yml`. Deploy via GitHub Actions atau manual:
Atau manual:
```bash ```bash
ssh mytheclipse@100.96.248.86 ssh mytheclipse@100.96.248.86
mkdir -p /opt/telemetry mkdir -p /opt/telemetry
# ... sync files from telemetry/ directory ...
cd /opt/telemetry cd /opt/telemetry
docker compose up -d docker compose up -d
bash clickhouse/init.sh bash clickhouse/init.sh
``` ```
## Port yang dibuka ---
## 4. Port yang Dibuka
### App VPS (imrnes) ### App VPS (imrnes)
| Port | Service | Akses | | Port | Service | Akses |
|------|---------|-------| |---|---|---|
| 80/443 | Web (via Traefik/Coolify) | Public | | 80/443 | Web (via Traefik/Coolify) | Public |
| 3000 | API metrics | Tailscale-only | | 3000 | API metrics | Tailscale-only |
| 8000 | ML service metrics | Tailscale-only | | 8000 | ML service metrics | Tailscale-only |
| 9100 | Node Exporter | Tailscale-only | | 9100 | Node Exporter | Tailscale-only |
### Telemetry VPS (orange) ### Telemetry VPS (orange)
| Port | Service | Akses | | Port | Service | Akses |
|------|---------|-------| |---|---|---|
| 80/443 | Telemetry UI (via Coolify Traefik) | Public | | 80/443 | Telemetry UI (via Coolify Traefik) | Public |
| 8181 | Telemetry UI (direct) | Tailscale-only | | 8181 | Telemetry UI (direct) | Tailscale-only |
| 9090 | Prometheus | Tailscale-only | | 9090 | Prometheus | Tailscale-only |
@@ -114,27 +144,44 @@ bash clickhouse/init.sh
| 8123 | ClickHouse HTTP | Tailscale-only | | 8123 | ClickHouse HTTP | Tailscale-only |
| 9000 | ClickHouse Native | Tailscale-only | | 9000 | ClickHouse Native | Tailscale-only |
## Metrics Flow ---
1. **App services** expose `/metrics` pada port masing-masing ## 5. Metrics Flow
1. **App services** mengekspos `GET /metrics` di port masing-masing
2. **Prometheus** di orange VPS scrape via Tailscale IP (`100.108.1.124:PORT`) 2. **Prometheus** di orange VPS scrape via Tailscale IP (`100.108.1.124:PORT`)
3. **Prometheus** forward ke **Metric Ingester** via `remote_write` 3. Prometheus forward ke **Metric Ingester** via `remote_write`
4. **Metric Ingester** enrich → filter → forward ke **Vector** 4. Metric Ingester enrich → filter → forward ke **Vector**
5. **Vector** buffer → write ke **ClickHouse** 5. Vector buffer → write ke **ClickHouse**
6. **Telemetry UI** query via **Query Proxy** → **ClickHouse** 6. **Telemetry UI** query via **Query Proxy** → **ClickHouse**
## Useful Commands ```
App Services (/metrics)
▼ (scrape via Tailscale)
Prometheus ──(remote_write)──► Metric Ingester ──► Vector ──► ClickHouse
Query Proxy ◄── Telemetry UI
```
---
## 6. Perintah Penting
```bash ```bash
# Telemetry stack status # Status telemetry stack
make telemetry-status make telemetry-status
# View telemetry logs # Lihat log service tertentu
make telemetry-logs s=prometheus make telemetry-logs s=prometheus
# Send test metric # Kirim test metric
make telemetry-test-metric make telemetry-test-metric
# Restart a service # Restart service
make telemetry-restart s=vector make telemetry-restart s=vector
``` ```
---
← [Kembali ke README utama](../README.md) &bull; [ML Service →](../apps/ml-service/README.md) &bull; [Pipeline ML →](../Machine_Learning/README.md)
+5 -1
View File
@@ -13,5 +13,9 @@
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
"packages/*" "packages/*"
] ],
"dependencies": {
"@tauri-apps/plugin-deep-link": "2.4.9",
"@tauri-apps/plugin-opener": "2.5.4"
}
} }