Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1732f5111 | ||
|
|
fc832b404e | ||
|
|
6ba3deb149 | ||
|
|
ac59548337 | ||
|
|
d292bc4b3c | ||
|
|
787acf077f | ||
|
|
250355562f | ||
|
|
d47c73308e | ||
|
|
4b1d70d1c4 |
@@ -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 |
@@ -179,6 +179,9 @@ jobs:
|
||||
SESSION_SECRET=${{ secrets.SESSION_SECRET }}
|
||||
WEB_APP_URL=https://zeavisedu.asepharyana.my.id
|
||||
ML_SERVICE_URL=http://zeavis-ml:8000
|
||||
GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
GOOGLE_REDIRECT_URI=https://zeavisedu.asepharyana.my.id/api/v1/auth/google/callback
|
||||
ENVEOF
|
||||
} > .env
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
---
|
||||
|
||||
@@ -391,3 +393,7 @@ export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
← [Kembali ke README utama](../README.md) • [ML Service →](../apps/ml-service/README.md)
|
||||
|
||||
@@ -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 • EfficientNetV2B0 • 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> •
|
||||
<a href="#-tim"><b>Tim</b></a> •
|
||||
<a href="#-ringkasan-eksekutif"><b>Ringkasan</b></a> •
|
||||
<a href="#-cakupan--deliverables"><b>Cakupan</b></a> •
|
||||
<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="#-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 |
|
||||
| Hawar Daun | Northern/Southern Leaf Blight |
|
||||
| Karat Daun | Common Rust |
|
||||
| Daun Sehat | Daun jagung tanpa gejala penyakit |
|
||||
| Arsitektur Model | **EfficientNetV2B0** — keseimbangan optimal antara akurasi dan efisiensi parameter |
|
||||
| Metode Pelatihan | **Transfer Learning** pada Google Colab (GPU T4) |
|
||||
| Sumber Dataset | Kaggle — [Corn Leaf Disease](https://www.kaggle.com/datasets/ndisan/corn-leaf-disease) |
|
||||
| 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** | 11–17 Mei 2026 | Inisiasi & Data | Spesifikasi teknis (Asep) • Dataset dari Kaggle + preprocessing (Taufik) • Wireframe UI/UX (Selly) |
|
||||
| **2** | 18–24 Mei 2026 | Training & Dev Awal | Implementasi EfficientNetV2B0 di Colab (Luhung) • Slicing UI ke React (Selly) • Setup server, database, routing API (Asep) |
|
||||
| **3** | 25–31 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** | 1–7 Juni 2026 | Integrasi & Testing | Integrasi penuh Front-End ↔ API ↔ Model ML • Pengujian end-to-end • Stress testing & error handling (Semua) |
|
||||
| **5** | 8–14 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/
|
||||
│ ├── api/ # Backend Elysia/Bun
|
||||
│ ├── ml-service/ # Layanan inferensi Rust/Axum + ONNX Runtime
|
||||
│ └── web/ # Frontend React + Vite
|
||||
├── Machine_Learning/ # Pipeline dataset, training, dan ekspor model
|
||||
├── packages/
|
||||
│ └── shared/ # Tipe dan utilitas bersama TypeScript
|
||||
│ ├── api/ # Backend Elysia/Bun + Drizzle ORM + PostgreSQL
|
||||
│ ├── ml-service/ # Rust/Axum + ONNX Runtime inference service
|
||||
│ └── web/ # Frontend React + Vite + Tailwind CSS
|
||||
├── Machine_Learning/ # Pipeline dataset, training Colab, ekspor model
|
||||
│ └── README.md # ⤷ Panduan lengkap pipeline ML
|
||||
├── infra/
|
||||
│ └── README.md # ⤷ Panduan deployment multi-VPS
|
||||
├── packages/shared/ # Tipe & utilitas TypeScript bersama
|
||||
├── telemetry/ # Submodule — Prometheus → ClickHouse pipeline
|
||||
├── docker-compose.yml # Konfigurasi deployment container
|
||||
├── package.json # Script dan workspace root Bun
|
||||
└── README.md # Dokumentasi utama proyek
|
||||
├── 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
|
||||
|
||||
- React
|
||||
- Vite
|
||||
- TypeScript
|
||||
- React Router
|
||||
- TanStack Query
|
||||
- Zustand
|
||||
- Tailwind CSS
|
||||
React • Vite • TypeScript • React Router • TanStack Query • Zustand • Tailwind CSS
|
||||
|
||||
### Backend API
|
||||
|
||||
- Bun
|
||||
- Elysia
|
||||
- Drizzle ORM
|
||||
- PostgreSQL
|
||||
Bun • Elysia • Drizzle ORM • PostgreSQL • prom-client
|
||||
|
||||
### Machine Learning
|
||||
Python • TensorFlow/Keras • EfficientNetV2B0 • Google Colab (GPU T4)
|
||||
Rust • Axum • ONNX Runtime • TFLite • TensorFlow.js
|
||||
|
||||
- Python (preprocessing, training, export)
|
||||
- TensorFlow/Keras
|
||||
- EfficientNetV2B0
|
||||
- Rust
|
||||
- Axum
|
||||
- ONNX Runtime
|
||||
- TFLite
|
||||
- TensorFlow.js
|
||||
### DevOps & Infrastruktur
|
||||
Docker • Docker Compose • Coolify • Traefik • Tailscale • GitHub Actions (CI/CD)
|
||||
|
||||
### Tooling & Deployment
|
||||
### Observabilitas
|
||||
Prometheus • Metric Ingester (Go) • Vector • ClickHouse • Query Proxy (Go) • 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
|
||||
- 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
|
||||
|
||||
## Prasyarat
|
||||
- **Bun** — runtime & package manager
|
||||
- **Python 3.9–3.11** — pipeline ML
|
||||
- **Rust & Cargo** — `apps/ml-service`
|
||||
- **Docker & Docker Compose** — deployment & telemetry
|
||||
- **PostgreSQL** — backend API
|
||||
|
||||
Untuk menjalankan seluruh project secara lokal, siapkan:
|
||||
|
||||
- Bun
|
||||
- Python 3.9–3.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:
|
||||
### Instalasi
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ATLAS-PJK-GM007/ZeaVis-Edu.git
|
||||
cd ZeaVis-Edu
|
||||
bun install
|
||||
```
|
||||
|
||||
## Menjalankan Project Lokal
|
||||
|
||||
### Menjalankan Semua Task Development
|
||||
### Menjalankan Development
|
||||
|
||||
```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
|
||||
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 |
|
||||
| Variable | Keterangan |
|
||||
|---|---|
|
||||
| `Machine_Learning/dataset.zip` | Dataset siap upload ke Colab |
|
||||
| `Machine_Learning/best_model/best_model.keras` | Model Keras hasil training |
|
||||
| `Machine_Learning/model/saved_model/` | TensorFlow SavedModel |
|
||||
| `Machine_Learning/model/model.tflite` | Model untuk mobile/TFLite |
|
||||
| `Machine_Learning/model/model.onnx` | Model untuk Rust ONNX Runtime |
|
||||
| `Machine_Learning/model/tfjs_model/` | Model untuk TensorFlow.js |
|
||||
| `DATABASE_URL` | URL koneksi PostgreSQL |
|
||||
| `SESSION_SECRET` | Secret untuk session auth |
|
||||
| `WEB_APP_URL` | URL frontend (untuk CORS) |
|
||||
| `ML_SERVICE_URL` | URL layanan inferensi ML |
|
||||
|
||||
## 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`
|
||||
- `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/`
|
||||
> 📖 **Panduan lengkap:** [`Machine_Learning/README.md`](Machine_Learning/README.md)
|
||||
|
||||
## Environment Variable Penting
|
||||
|
||||
| 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:
|
||||
### Deployment
|
||||
|
||||
```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
|
||||
MODEL_PATH=/path/to/model.onnx cargo run
|
||||
```
|
||||
| Masalah | Solusi |
|
||||
|---|---|
|
||||
| `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
|
||||
docker network create app-shared-net
|
||||
```
|
||||
1. Prayogi, A. et al. *"Klasifikasi Penyakit Daun Jagung Menggunakan CNN"* — [SISTEMATIS](https://ejournal.rizaniamedia.com/index.php/sistematis/article/view/87/49)
|
||||
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:
|
||||
|
||||
```bash
|
||||
export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
|
||||
```
|
||||
|
||||
## 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.
|
||||
<p align="center">
|
||||
<sub>
|
||||
Capstone Project • Pijak × IBM SkillsBuild • AI for Smart Education<br>
|
||||
© 2026 ZeaVis Edu Team
|
||||
</sub>
|
||||
</p>
|
||||
|
||||
+69
-18
@@ -55,9 +55,6 @@ async function exchangeGoogleCode(code: string): Promise<GoogleTokenResponse> {
|
||||
}
|
||||
|
||||
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('.');
|
||||
if (parts.length !== 3) {
|
||||
throw new Error('Invalid id_token format');
|
||||
@@ -66,6 +63,41 @@ function decodeGoogleIdToken(idToken: string): GoogleIdPayload {
|
||||
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, '"')}" 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, '<')}</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) {
|
||||
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);
|
||||
return { ok: true };
|
||||
})
|
||||
.get('/google', ({ set }) => {
|
||||
.get('/google', ({ query, set }) => {
|
||||
if (!env.googleOAuthEnabled) {
|
||||
set.status = 404;
|
||||
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({
|
||||
client_id: env.googleClientId!,
|
||||
redirect_uri: env.googleRedirectUri!,
|
||||
@@ -184,9 +219,11 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
||||
scope: 'openid email profile',
|
||||
access_type: 'offline',
|
||||
prompt: 'select_account',
|
||||
state,
|
||||
});
|
||||
|
||||
set.redirect = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
||||
set.status = 302;
|
||||
set.headers['Location'] = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
||||
})
|
||||
.get('/google/callback', async ({ query, set, request }) => {
|
||||
if (!env.googleOAuthEnabled) {
|
||||
@@ -194,12 +231,20 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
||||
return { error: 'Google OAuth is not configured' };
|
||||
}
|
||||
|
||||
const code = (query as Record<string, string>).code;
|
||||
const error = (query as Record<string, string>).error;
|
||||
const q = query as Record<string, string>;
|
||||
const code = q.code;
|
||||
const error = q.error;
|
||||
const platform = resolvePlatform(q.state);
|
||||
|
||||
// User denied or Google returned an error
|
||||
const makeErrorUrl = (msg: string) =>
|
||||
`${env.webAppUrl}/login?error=${encodeURIComponent(msg)}`;
|
||||
|
||||
if (error || !code) {
|
||||
set.redirect = `${env.webAppUrl}/login?error=${encodeURIComponent(error ?? 'missing_code')}`;
|
||||
const url = makeErrorUrl(error ?? 'missing_code');
|
||||
if (platform === 'tauri') return renderTauriDeepLinkPage(url);
|
||||
set.status = 302;
|
||||
set.headers['Location'] = url;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -210,13 +255,19 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
||||
idPayload = decodeGoogleIdToken(tokens.id_token);
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : 'Google auth failed';
|
||||
set.redirect = `${env.webAppUrl}/login?error=${encodeURIComponent(msg)}`;
|
||||
const url = makeErrorUrl(msg);
|
||||
if (platform === 'tauri') return renderTauriDeepLinkPage(url);
|
||||
set.status = 302;
|
||||
set.headers['Location'] = url;
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate email
|
||||
if (!idPayload.email_verified || !idPayload.email) {
|
||||
set.redirect = `${env.webAppUrl}/login?error=${encodeURIComponent('Email not verified by Google')}`;
|
||||
const url = makeErrorUrl('Email not verified by Google');
|
||||
if (platform === 'tauri') return renderTauriDeepLinkPage(url);
|
||||
set.status = 302;
|
||||
set.headers['Location'] = url;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -227,19 +278,15 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
||||
try {
|
||||
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);
|
||||
|
||||
// 2. If not found, try by email (link existing account)
|
||||
if (!user) {
|
||||
user = await db.select().from(users).where(eq(users.email, email)).limit(1).then(r => r[0] ?? null);
|
||||
if (user) {
|
||||
// Link googleId to existing account
|
||||
await db.update(users).set({ googleId }).where(eq(users.id, user.id));
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Create new user if nothing matched
|
||||
if (!user) {
|
||||
const inserted = await db
|
||||
.insert(users)
|
||||
@@ -249,15 +296,19 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
||||
authCounter.labels('register', 'true').inc();
|
||||
}
|
||||
|
||||
// Create session
|
||||
const token = await createSession(user.id);
|
||||
set.headers['Set-Cookie'] = createSessionCookie(token, request.headers);
|
||||
|
||||
authCounter.labels('login', 'true').inc();
|
||||
|
||||
// Redirect to web app with token in URL for localStorage fallback
|
||||
set.redirect = `${env.webAppUrl}/login?token=${encodeURIComponent(token)}`;
|
||||
const successUrl = `${env.webAppUrl}/login?token=${encodeURIComponent(token)}`;
|
||||
if (platform === 'tauri') return renderTauriDeepLinkPage(successUrl);
|
||||
set.status = 302;
|
||||
set.headers['Location'] = successUrl;
|
||||
} catch (err) {
|
||||
set.redirect = `${env.webAppUrl}/login?error=${encodeURIComponent('Database unavailable')}`;
|
||||
const url = makeErrorUrl('Database unavailable');
|
||||
if (platform === 'tauri') return renderTauriDeepLinkPage(url);
|
||||
set.status = 302;
|
||||
set.headers['Location'] = url;
|
||||
}
|
||||
});
|
||||
|
||||
+89
-155
@@ -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)
|
||||
- **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
|
||||
- **Multipart Upload:** Dukungan upload gambar langsung via HTTP POST
|
||||
|
||||
## Prasyarat
|
||||
---
|
||||
|
||||
## 2. Prasyarat & Instalasi
|
||||
|
||||
- Rust 1.70+ dan Cargo
|
||||
- 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.
|
||||
|
||||
```bash
|
||||
@@ -27,75 +42,58 @@ cargo build
|
||||
|
||||
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`.
|
||||
|
||||
### Opsi 1: Default (Port 8000, Model dari Machine_Learning/)
|
||||
### Opsi 1: Default (Port 8000)
|
||||
|
||||
```bash
|
||||
cd apps/ml-service
|
||||
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
|
||||
```
|
||||
|
||||
### Opsi 2: Local Development dengan .env.example (Port 8001)
|
||||
|
||||
Untuk development lokal dengan port 8001 (sesuai `.env.example`):
|
||||
|
||||
```bash
|
||||
cd apps/ml-service
|
||||
source .env.example
|
||||
cargo run
|
||||
```
|
||||
|
||||
Service akan mendengarkan di `http://localhost:8001` karena `ML_SERVICE_PORT=8001` di `.env.example`.
|
||||
|
||||
### Opsi 3: Custom Model Path
|
||||
|
||||
Jika model berada di lokasi lain, gunakan environment variable `MODEL_PATH`:
|
||||
### Opsi 3: Custom Model Path & Port
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
---
|
||||
|
||||
## 4. Environment Variables
|
||||
|
||||
| Variable | Default | Keterangan |
|
||||
|---|---|---|
|
||||
| `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_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) |
|
||||
|
||||
## Endpoint API
|
||||
---
|
||||
|
||||
### 1. Health Check
|
||||
## 5. Endpoint API
|
||||
|
||||
### Health Check
|
||||
|
||||
**Default (port 8000):**
|
||||
```bash
|
||||
curl http://localhost:8000/health
|
||||
```
|
||||
|
||||
**Local dev dengan .env.example (port 8001):**
|
||||
```bash
|
||||
curl http://localhost:8001/health
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
@@ -103,19 +101,12 @@ curl http://localhost:8001/health
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Metadata
|
||||
### Metadata
|
||||
|
||||
**Default (port 8000):**
|
||||
```bash
|
||||
curl http://localhost:8000/metadata
|
||||
```
|
||||
|
||||
**Local dev dengan .env.example (port 8001):**
|
||||
```bash
|
||||
curl http://localhost:8001/metadata
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"service_name": "zeavis-ml-service",
|
||||
@@ -123,32 +114,19 @@ curl http://localhost:8001/metadata
|
||||
"model_path": "../../Machine_Learning/model/model.onnx",
|
||||
"model_loaded": true,
|
||||
"input_size": 224,
|
||||
"labels": [
|
||||
"Bercak Daun",
|
||||
"Daun Sehat",
|
||||
"Karat Daun",
|
||||
"Hawar Daun"
|
||||
]
|
||||
"labels": ["Bercak Daun", "Daun Sehat", "Karat Daun", "Hawar Daun"]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Prediksi
|
||||
### Prediksi
|
||||
|
||||
Upload gambar daun jagung untuk klasifikasi:
|
||||
|
||||
**Default (port 8000):**
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/predict \
|
||||
-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
|
||||
{
|
||||
"label": "Daun Sehat",
|
||||
@@ -162,120 +140,45 @@ curl -X POST http://localhost:8001/predict \
|
||||
}
|
||||
```
|
||||
|
||||
## Verifikasi & Testing
|
||||
---
|
||||
|
||||
## 6. Verifikasi & Testing
|
||||
|
||||
### Build Produksi
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
# Binary di target/release/zeavis-ml-service
|
||||
```
|
||||
|
||||
Output binary akan tersedia di `target/release/zeavis-ml-service`.
|
||||
|
||||
### Menjalankan Tests
|
||||
|
||||
```bash
|
||||
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
|
||||
# 1. Start service
|
||||
cargo run
|
||||
```
|
||||
|
||||
2. Di terminal lain, test health endpoint:
|
||||
```bash
|
||||
# 2. Health check
|
||||
curl http://localhost:8000/health
|
||||
```
|
||||
|
||||
3. Test metadata:
|
||||
```bash
|
||||
# 3. Metadata
|
||||
curl http://localhost:8000/metadata
|
||||
```
|
||||
|
||||
4. Test prediksi dengan gambar sample:
|
||||
```bash
|
||||
# 4. Prediksi
|
||||
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
|
||||
```
|
||||
## 7. Docker Deployment
|
||||
|
||||
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
|
||||
ML_SERVICE_PORT=9000 cargo run
|
||||
```
|
||||
|
||||
Atau jika menggunakan `.env.example` (port 8001), pastikan tidak ada service lain di port tersebut:
|
||||
|
||||
```bash
|
||||
lsof -i :8001
|
||||
```
|
||||
|
||||
### ONNX Runtime tidak kompatibel
|
||||
|
||||
**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.
|
||||
Service dapat di-deploy via Docker. Build dari root repository karena Dockerfile menyalin source service dan artifact ONNX dari beberapa direktori repo.
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
### 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
|
||||
- [`README.md`](../../README.md) — Dokumentasi proyek utama
|
||||
**Error:** `Failed to load model: No such file or directory`
|
||||
|
||||
**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) • [Pipeline ML →](../../Machine_Learning/README.md) • [Infra →](../../infra/README.md)
|
||||
|
||||
@@ -12,5 +12,7 @@ tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
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_json = "1"
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default"
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"opener:allow-open-url",
|
||||
"deep-link:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/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.
|
||||
set -euo pipefail
|
||||
|
||||
@@ -10,11 +12,34 @@ if [ ! -f "$MANIFEST" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -q 'android.permission.CAMERA' "$MANIFEST"; then
|
||||
echo "CAMERA permission already present in AndroidManifest.xml"
|
||||
exit 0
|
||||
fi
|
||||
# ── CAMERA permission ──────────────────────────────────────────────────
|
||||
|
||||
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"
|
||||
echo "Done."
|
||||
else
|
||||
echo "CAMERA permission already present."
|
||||
fi
|
||||
|
||||
# ── Deep link intent filter ────────────────────────────────────────────
|
||||
# Allows the app to receive zeavisedu:// scheme URLs from the system browser
|
||||
# (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."
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { FormEvent, useState } from 'react';
|
||||
import { FormEvent, useState, useCallback } from 'react';
|
||||
import { Eye, EyeOff } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { isTauri, openUrl } from '@/lib/tauri';
|
||||
|
||||
type AuthFormProps = {
|
||||
mode: 'login' | 'register';
|
||||
@@ -25,6 +26,16 @@ export function AuthForm({ mode, isSubmitting, error, googleOAuthEnabled, onSubm
|
||||
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 (
|
||||
<Card className="mx-auto w-full max-w-md">
|
||||
<CardHeader>
|
||||
@@ -75,8 +86,15 @@ export function AuthForm({ mode, isSubmitting, error, googleOAuthEnabled, onSubm
|
||||
</Button>
|
||||
</form>
|
||||
{googleOAuthEnabled && (
|
||||
<Button className="mt-3 w-full" variant="outline" asChild>
|
||||
<a href="/api/v1/auth/google">Masuk dengan Google</a>
|
||||
<Button className="mt-3 w-full flex items-center justify-center gap-2.5" variant="outline" onClick={handleGoogleLogin} type="button">
|
||||
<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="#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="#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" />
|
||||
</svg>
|
||||
Masuk dengan Google
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
// Lazy-import Tauri opener only in Tauri context
|
||||
const { openUrl: tauriOpenUrl } = await import('@tauri-apps/plugin-opener');
|
||||
await tauriOpenUrl(url);
|
||||
}
|
||||
@@ -1,23 +1,28 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AuthForm } from "@/components/auth-form";
|
||||
import { apiClient, setAuthToken } from "@/lib/api-client";
|
||||
import { useAuthStore } from "@/store/auth-store";
|
||||
|
||||
function getUrlParam(name: string): string | null {
|
||||
return new URLSearchParams(window.location.search).get(name);
|
||||
}
|
||||
|
||||
export function LoginPage() {
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const setUser = useAuthStore((state) => state.setUser);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchParams] = useSearchParams();
|
||||
const oauthTokenConsumed = useRef(false);
|
||||
const [oauthProcessing, setOauthProcessing] = useState(false);
|
||||
|
||||
// Handle OAuth callback: the API redirects to /login?token=<session_token>
|
||||
useEffect(() => {
|
||||
const token = searchParams.get("token");
|
||||
const token = getUrlParam("token");
|
||||
if (!token || oauthTokenConsumed.current) return;
|
||||
oauthTokenConsumed.current = true;
|
||||
setOauthProcessing(true);
|
||||
|
||||
// Store token for future API calls and fetch user
|
||||
setAuthToken(token);
|
||||
@@ -31,12 +36,13 @@ export function LoginPage() {
|
||||
})
|
||||
.catch((err) => {
|
||||
setAuthToken(null);
|
||||
setOauthProcessing(false);
|
||||
setError(err instanceof Error ? err.message : "Google login gagal");
|
||||
});
|
||||
}, [searchParams, setUser, queryClient, navigate]);
|
||||
}, [setUser, queryClient, navigate]);
|
||||
|
||||
// Show OAuth error from query param
|
||||
const oauthError = searchParams.get("error");
|
||||
const oauthError = getUrlParam("error");
|
||||
const meQuery = useQuery({
|
||||
queryKey: ["auth", "me"],
|
||||
queryFn: () => apiClient.getMe(),
|
||||
@@ -53,6 +59,18 @@ export function LoginPage() {
|
||||
setError(err instanceof Error ? err.message : "Login gagal"),
|
||||
});
|
||||
|
||||
// Show loading spinner while OAuth token is being processed
|
||||
if (oauthProcessing) {
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center px-6 py-12">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<div className="h-10 w-10 border-4 border-green-500 border-t-transparent rounded-full animate-spin" />
|
||||
<p className="text-gray-500 text-sm">Menyelesaikan login dengan Google...</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center px-6 py-12">
|
||||
<div className="w-full max-w-sm md:max-w-md space-y-4">
|
||||
|
||||
+16
-1
@@ -9,7 +9,22 @@ export default defineConfig(({ mode }) => {
|
||||
const apiProxyTarget = env.VITE_API_PROXY_TARGET || 'http://localhost:3000';
|
||||
|
||||
return {
|
||||
plugins: [react(), tsconfigPaths(), metricsPlugin()],
|
||||
plugins: [
|
||||
react(),
|
||||
tsconfigPaths(),
|
||||
metricsPlugin(),
|
||||
{
|
||||
name: 'cloudflare-rocket-loader-fix',
|
||||
transformIndexHtml(html) {
|
||||
// Prevent Cloudflare Rocket Loader from mangling <script type="module">
|
||||
// which breaks the entire JS bundle (blank page)
|
||||
return html.replace(
|
||||
/<script type="module"/g,
|
||||
'<script data-cfasync="false" type="module"',
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': apiProxyTarget,
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "zeavis-edu",
|
||||
"dependencies": {
|
||||
"@tauri-apps/plugin-deep-link": "2.4.9",
|
||||
"@tauri-apps/plugin-opener": "2.5.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@moonrepo/cli": "^2.2.5",
|
||||
"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=="],
|
||||
|
||||
"@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-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/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/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||
|
||||
+80
-33
@@ -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`):**
|
||||
| Secret | Value |
|
||||
|--------|-------|
|
||||
## 2. Prasyarat GitHub Secrets
|
||||
|
||||
### App VPS — `.github/workflows/deploy.yml`
|
||||
|
||||
| Secret | Keterangan |
|
||||
|---|---|
|
||||
| `VPS_HOST` | `100.108.1.124` (imrnes) |
|
||||
| `VPS_USER` | `mytheclipse` |
|
||||
| `VPS_SSH_KEY` | Private SSH key for imrnes |
|
||||
| `VPS_SSH_KEY` | Private SSH key untuk imrnes |
|
||||
| `VPS_PORT` | `22` |
|
||||
| `DATABASE_URL` | PostgreSQL connection string |
|
||||
| `SESSION_SECRET` | Random session secret |
|
||||
|
||||
**Telemetry VPS deploy (`.github/workflows/telemetry-ci-cd.yml`):**
|
||||
| Secret | Value |
|
||||
|--------|-------|
|
||||
### Telemetry VPS — `.github/workflows/telemetry-ci-cd.yml`
|
||||
|
||||
| Secret | Keterangan |
|
||||
|---|---|
|
||||
| `TELEMETRY_VPS_HOST` | `100.96.248.86` (orange) |
|
||||
| `TELEMETRY_VPS_USER` | SSH username for orange |
|
||||
| `TELEMETRY_VPS_SSH_KEY` | Private SSH key for orange |
|
||||
| `TELEMETRY_VPS_USER` | SSH username |
|
||||
| `TELEMETRY_VPS_SSH_KEY` | Private SSH key |
|
||||
| `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
|
||||
# Create Docker network
|
||||
@@ -79,33 +107,35 @@ docker network create telemetry-net
|
||||
# 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
|
||||
ssh mytheclipse@100.96.248.86
|
||||
mkdir -p /opt/telemetry
|
||||
# ... sync files from telemetry/ directory ...
|
||||
cd /opt/telemetry
|
||||
docker compose up -d
|
||||
bash clickhouse/init.sh
|
||||
```
|
||||
|
||||
## Port yang dibuka
|
||||
---
|
||||
|
||||
## 4. Port yang Dibuka
|
||||
|
||||
### App VPS (imrnes)
|
||||
|
||||
| Port | Service | Akses |
|
||||
|------|---------|-------|
|
||||
|---|---|---|
|
||||
| 80/443 | Web (via Traefik/Coolify) | Public |
|
||||
| 3000 | API metrics | Tailscale-only |
|
||||
| 8000 | ML service metrics | Tailscale-only |
|
||||
| 9100 | Node Exporter | Tailscale-only |
|
||||
|
||||
### Telemetry VPS (orange)
|
||||
|
||||
| Port | Service | Akses |
|
||||
|------|---------|-------|
|
||||
|---|---|---|
|
||||
| 80/443 | Telemetry UI (via Coolify Traefik) | Public |
|
||||
| 8181 | Telemetry UI (direct) | Tailscale-only |
|
||||
| 9090 | Prometheus | Tailscale-only |
|
||||
@@ -114,27 +144,44 @@ bash clickhouse/init.sh
|
||||
| 8123 | ClickHouse HTTP | 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`)
|
||||
3. **Prometheus** forward ke **Metric Ingester** via `remote_write`
|
||||
4. **Metric Ingester** enrich → filter → forward ke **Vector**
|
||||
5. **Vector** buffer → write ke **ClickHouse**
|
||||
3. Prometheus forward ke **Metric Ingester** via `remote_write`
|
||||
4. Metric Ingester enrich → filter → forward ke **Vector**
|
||||
5. Vector buffer → write ke **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
|
||||
# Telemetry stack status
|
||||
# Status telemetry stack
|
||||
make telemetry-status
|
||||
|
||||
# View telemetry logs
|
||||
# Lihat log service tertentu
|
||||
make telemetry-logs s=prometheus
|
||||
|
||||
# Send test metric
|
||||
# Kirim test metric
|
||||
make telemetry-test-metric
|
||||
|
||||
# Restart a service
|
||||
# Restart service
|
||||
make telemetry-restart s=vector
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
← [Kembali ke README utama](../README.md) • [ML Service →](../apps/ml-service/README.md) • [Pipeline ML →](../Machine_Learning/README.md)
|
||||
|
||||
+5
-1
@@ -13,5 +13,9 @@
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
]
|
||||
],
|
||||
"dependencies": {
|
||||
"@tauri-apps/plugin-deep-link": "2.4.9",
|
||||
"@tauri-apps/plugin-opener": "2.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user