Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4bd114eed | ||
|
|
7423e4d83b | ||
|
|
4b252a1d5d | ||
|
|
7148e1026e | ||
|
|
96f337ef46 | ||
|
|
3b5839ebd5 | ||
|
|
e2742a7a60 | ||
|
|
ae22adee40 | ||
|
|
ee02bd4f9a | ||
|
|
310d8adc8d | ||
|
|
ccbcd99053 | ||
|
|
52e60b99dd | ||
|
|
e19321a2b7 | ||
|
|
d1275b5b65 | ||
|
|
12162fd6f7 | ||
|
|
9ab8d41adf | ||
|
|
6f07fc05ac | ||
|
|
cbcc7da33d | ||
|
|
d1282364e7 | ||
|
|
254fe19bb6 | ||
|
|
99acc43ff0 | ||
|
|
1afd2e013c | ||
|
|
e1732f5111 | ||
|
|
fc832b404e | ||
|
|
6ba3deb149 | ||
|
|
ac59548337 | ||
|
|
d292bc4b3c | ||
|
|
787acf077f |
@@ -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 |
@@ -136,7 +136,11 @@ jobs:
|
||||
rm -rf gen/android
|
||||
bun tauri android init
|
||||
|
||||
- name: Patch AndroidManifest (CAMERA permission)
|
||||
- name: Generate Android launcher icons from SVG logo
|
||||
working-directory: apps/tauri
|
||||
run: bun run scripts/generate-icons.js
|
||||
|
||||
- name: Patch AndroidManifest (CAMERA permission + deep link)
|
||||
working-directory: apps/tauri
|
||||
run: bash scripts/patch-android-manifest.sh
|
||||
|
||||
|
||||
@@ -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
|
||||
**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) • [ML Service →](../apps/ml-service/README.md)
|
||||
|
||||
@@ -1,482 +1,326 @@
|
||||
# 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 • Rust ONNX Runtime • Tauri 2 Android</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="#-platform"><b>Platform</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 + Android (Tauri 2) | Aplikasi iOS |
|
||||
|
||||
### 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 + Android (Tauri 2) 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
|
||||
├── docker-compose.yml # Konfigurasi deployment container
|
||||
├── package.json # Script dan workspace root Bun
|
||||
└── README.md # Dokumentasi utama proyek
|
||||
│ ├── api/ # Backend Elysia/Bun + Drizzle ORM + PostgreSQL
|
||||
│ ├── ml-service/ # Rust/Axum + ONNX Runtime inference engine
|
||||
│ ├── tauri/ # Tauri 2 mobile wrapper → Android APK
|
||||
│ └── web/ # Frontend React + Vite + Tailwind CSS
|
||||
├── Machine_Learning/ # Pipeline dataset, training Colab, ekspor model
|
||||
│ └── README.md # ⤷ Panduan lengkap pipeline ML
|
||||
├── 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 # Root workspace Bun + Moon
|
||||
└── README.md # ⤷ Anda di sini
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
| Komponen | Teknologi | Dokumentasi |
|
||||
|---|---|---|
|
||||
| Web Frontend | React, Vite, Tailwind, Zustand, TanStack Query | `apps/web/` |
|
||||
| Android App | Tauri 2, Rust, WebView, Deep Link OAuth | `apps/tauri/` |
|
||||
| API Backend | Bun, Elysia, Drizzle ORM, PostgreSQL | `apps/api/` |
|
||||
| ML Inference Engine | Rust, Axum, ONNX Runtime | [`apps/ml-service/README.md`](apps/ml-service/README.md) |
|
||||
| ML Pipeline | Python, TensorFlow/Keras, EfficientNetV2B0 | [`Machine_Learning/README.md`](Machine_Learning/README.md) |
|
||||
| Infrastruktur | Docker, Coolify, Traefik, Tailscale | [`infra/README.md`](infra/README.md) |
|
||||
| Telemetry | Prometheus, ClickHouse, Vector, Vue 3 | `telemetry/` |
|
||||
|
||||
### Frontend
|
||||
---
|
||||
|
||||
- React
|
||||
- Vite
|
||||
- TypeScript
|
||||
- React Router
|
||||
- TanStack Query
|
||||
- Zustand
|
||||
- Tailwind CSS
|
||||
## 🛠️ Tech Stack
|
||||
|
||||
### Frontend & Mobile
|
||||
React • Vite • TypeScript • React Router • TanStack Query • Zustand • Tailwind CSS
|
||||
**Tauri 2** (Android) • Rust • WebView • Deep Link OAuth
|
||||
|
||||
### Backend API
|
||||
|
||||
- Bun
|
||||
- Elysia
|
||||
- Drizzle ORM
|
||||
- PostgreSQL
|
||||
Bun • Elysia • Drizzle ORM • PostgreSQL • prom-client
|
||||
|
||||
### Machine Learning
|
||||
Python • TensorFlow/Keras • EfficientNetV2B0 • Google Colab (GPU T4)
|
||||
|
||||
- Python (preprocessing, training, export)
|
||||
- TensorFlow/Keras
|
||||
- EfficientNetV2B0
|
||||
- Rust
|
||||
- Axum
|
||||
- ONNX Runtime
|
||||
- TFLite
|
||||
- TensorFlow.js
|
||||
### Inference Engine
|
||||
**Rust** • **Axum** • **ONNX Runtime** • TFLite • TensorFlow.js
|
||||
|
||||
### Tooling & Deployment
|
||||
### DevOps & Infrastruktur
|
||||
Docker • Docker Compose • Coolify • Traefik • Tailscale • GitHub Actions (CI/CD)
|
||||
|
||||
- Bun workspaces
|
||||
- Moon task runner
|
||||
- Docker
|
||||
- Docker Compose
|
||||
- GitHub Container Registry
|
||||
- Traefik labels untuk routing deployment
|
||||
### Observabilitas
|
||||
Prometheus • Metric Ingester (Go) • Vector • ClickHouse • Query Proxy (Go) • Telemetry UI (Vue 3)
|
||||
|
||||
### Telemetry & Observability
|
||||
---
|
||||
|
||||
- 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
|
||||
## 🚀 Memulai
|
||||
|
||||
## Prasyarat
|
||||
### Prasyarat
|
||||
|
||||
Untuk menjalankan seluruh project secara lokal, siapkan:
|
||||
- **Bun** — runtime & package manager
|
||||
- **Python 3.9–3.11** — pipeline ML
|
||||
- **Rust & Cargo** — `apps/ml-service` (inference) & `apps/tauri` (Android)
|
||||
- **Java 21 + Android SDK** — build Android APK
|
||||
- **Docker & Docker Compose** — deployment & telemetry
|
||||
- **PostgreSQL** — backend API
|
||||
|
||||
- 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 # ML inference engine (port 8000)
|
||||
cd apps/tauri && bun run tauri dev # Tauri desktop dev
|
||||
cd apps/tauri && bun run tauri android dev # Tauri Android dev
|
||||
```
|
||||
|
||||
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
|
||||
### Deployment
|
||||
|
||||
| Variable | Digunakan oleh | Keterangan |
|
||||
```bash
|
||||
docker compose up -d # App services
|
||||
make telemetry-up # Telemetry stack
|
||||
```
|
||||
|
||||
> 📖 **Panduan infrastruktur:** [`infra/README.md`](infra/README.md)
|
||||
|
||||
---
|
||||
|
||||
## 📱 Platform
|
||||
|
||||
ZeaVis Edu tersedia di **dua platform** dari satu codebase:
|
||||
|
||||
| Platform | Teknologi | Build |
|
||||
|---|---|---|
|
||||
| `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` |
|
||||
| **Web** | React + Vite → Static SPA | `bun run build` |
|
||||
| **Android** | Tauri 2 + Rust → WebView APK | `cd apps/tauri && bun run tauri android build --apk` |
|
||||
|
||||
## Troubleshooting
|
||||
### Tauri 2 Android
|
||||
|
||||
### `bun run dev` gagal karena dependency belum tersedia
|
||||
Aplikasi Android membungkus frontend web yang sama dalam **WebView native** menggunakan **Tauri 2**, memberikan akses ke API native Android tanpa menulis ulang UI.
|
||||
|
||||
Jalankan ulang instalasi dari root repository:
|
||||
**Fitur Android:**
|
||||
- **Google OAuth** — Login via system browser + deep link `zeavisedu://` kembali ke app
|
||||
- **Kamera** — Izin `CAMERA` untuk unggah foto daun jagung langsung dari kamera
|
||||
- **Tauri Plugin Opener** — Buka URL eksternal di system browser
|
||||
- **Tauri Plugin Deep Link** — Tangkap OAuth callback tanpa memerlukan server redirect
|
||||
|
||||
**CI/CD Android:**
|
||||
- GitHub Actions workflow `.github/workflows/android.yml`
|
||||
- Build otomatis di setiap push/PR ke `main`
|
||||
- Patch `AndroidManifest.xml` untuk menambahkan izin kamera + intent filter deep link
|
||||
- APK ditandatangani (signed) via `apksigner` + release ke GitHub Releases
|
||||
|
||||
```bash
|
||||
bun install
|
||||
# Development Android (butuh Android SDK + emulator/device)
|
||||
cd apps/tauri
|
||||
bun run tauri android init # Init project Android
|
||||
bun run tauri android dev # Dev dengan hot reload
|
||||
bun run tauri android build --apk # Build APK production
|
||||
|
||||
# CI/CD — dijalankan otomatis via GitHub Actions
|
||||
.github/workflows/android.yml
|
||||
```
|
||||
|
||||
### API membutuhkan database
|
||||
> Konfigurasi: `apps/tauri/tauri.conf.json` • `apps/tauri/gen/android/`
|
||||
|
||||
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>
|
||||
|
||||
+86
-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,58 @@ function decodeGoogleIdToken(idToken: string): GoogleIdPayload {
|
||||
return JSON.parse(payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a page for the Android system browser that uses Chrome's native
|
||||
* `intent://` protocol to open the Tauri app with the session URL.
|
||||
* Falls back to a clickable button if the intent is blocked.
|
||||
*/
|
||||
function renderTauriDeepLinkPage(targetUrl: string): Response {
|
||||
// Extract the path + query from the full URL for the intent
|
||||
let pathAndQuery = '/login';
|
||||
try {
|
||||
const u = new URL(targetUrl);
|
||||
pathAndQuery = u.pathname + u.search + u.hash;
|
||||
} catch { /* use default */ }
|
||||
|
||||
const displayUrl = targetUrl.replace(/"/g, '"');
|
||||
const escapedPath = pathAndQuery.replace(/"/g, '"');
|
||||
// intent:// scheme: Chrome on Android opens the target app by package name
|
||||
// browser_fallback_url: shown if the app isn't installed
|
||||
// Use intent://login/... to produce data URI zeavisedu://login/login?token=xxx
|
||||
// which new URL() can parse (single-slash non-hierarchical URLs break WebView)
|
||||
const intentUrl = `intent://login${escapedPath}#Intent;scheme=zeavisedu;package=com.zeavis.edu;S.browser_fallback_url=${encodeURIComponent(targetUrl)};end`;
|
||||
|
||||
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;max-width:360px">
|
||||
<p style="color:#166534;font-size:1.1rem;margin-bottom:1.5rem">Login Google berhasil!<br>Kembali ke aplikasi...</p>
|
||||
<a href="${intentUrl.replace(/"/g, '"')}" id="open-app" 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;margin-bottom:1rem">Buka ZeaVis Edu</a>
|
||||
<p style="color:#6b7280;font-size:0.8rem">Jika tombol di atas tidak berfungsi, salin dan buka URL ini di aplikasi ZeaVis Edu:</p>
|
||||
<code style="display:block;word-break:break-all;font-size:0.7rem;color:#4b5563;background:#e5e7eb;padding:0.5rem;border-radius:0.25rem;margin-top:0.5rem">${displayUrl.replace(/</g, '<').replace(/>/g, '>')}</code>
|
||||
</div>
|
||||
<script>
|
||||
// Auto-open the intent
|
||||
window.location.href = ${JSON.stringify(intentUrl)};
|
||||
</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 +220,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,6 +236,7 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
||||
scope: 'openid email profile',
|
||||
access_type: 'offline',
|
||||
prompt: 'select_account',
|
||||
state,
|
||||
});
|
||||
|
||||
set.status = 302;
|
||||
@@ -195,13 +248,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) {
|
||||
const url = makeErrorUrl(error ?? 'missing_code');
|
||||
if (platform === 'tauri') return renderTauriDeepLinkPage(url);
|
||||
set.status = 302;
|
||||
set.headers['Location'] = `${env.webAppUrl}/login?error=${encodeURIComponent(error ?? 'missing_code')}`;
|
||||
set.headers['Location'] = url;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -212,15 +272,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';
|
||||
const url = makeErrorUrl(msg);
|
||||
if (platform === 'tauri') return renderTauriDeepLinkPage(url);
|
||||
set.status = 302;
|
||||
set.headers['Location'] = `${env.webAppUrl}/login?error=${encodeURIComponent(msg)}`;
|
||||
set.headers['Location'] = url;
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate 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.headers['Location'] = `${env.webAppUrl}/login?error=${encodeURIComponent('Email not verified by Google')}`;
|
||||
set.headers['Location'] = url;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -231,19 +295,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)
|
||||
@@ -253,17 +313,25 @@ 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);
|
||||
const sessionCookie = createSessionCookie(token, request.headers);
|
||||
|
||||
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') {
|
||||
// Inject Set-Cookie into the response so the browser gets it on redirect
|
||||
const resp = renderTauriDeepLinkPage(successUrl);
|
||||
resp.headers.set('Set-Cookie', sessionCookie);
|
||||
return resp;
|
||||
}
|
||||
set.headers['Set-Cookie'] = sessionCookie;
|
||||
set.status = 302;
|
||||
set.headers['Location'] = `${env.webAppUrl}/login?token=${encodeURIComponent(token)}`;
|
||||
set.headers['Location'] = successUrl;
|
||||
} catch (err) {
|
||||
const url = makeErrorUrl('Database unavailable');
|
||||
if (platform === 'tauri') return renderTauriDeepLinkPage(url);
|
||||
set.status = 302;
|
||||
set.headers['Location'] = `${env.webAppUrl}/login?error=${encodeURIComponent('Database unavailable')}`;
|
||||
set.headers['Location'] = url;
|
||||
}
|
||||
});
|
||||
|
||||
+97
-163
@@ -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
|
||||
|
||||
#### 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:
|
||||
### Verifikasi Manual (default port 8000)
|
||||
|
||||
```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
|
||||
lsof -i :8001
|
||||
```
|
||||
## 7. Docker Deployment
|
||||
|
||||
### 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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,5 +12,8 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2"
|
||||
},
|
||||
"dependencies": {
|
||||
"sharp": "0.35.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generate Android launcher icons from the ZeaVis Edu logo SVG.
|
||||
* Produces PNGs at all required densities and replaces Tauri's default icons.
|
||||
*
|
||||
* Usage: node scripts/generate-icons.js
|
||||
* Requires: bun add sharp (already in devDependencies)
|
||||
*/
|
||||
|
||||
const sharp = require('sharp');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const LOGO = path.resolve(__dirname, '../../../.github/assets/zeavis-logo.svg');
|
||||
const RES = path.resolve(__dirname, '../gen/android/app/src/main/res');
|
||||
|
||||
// Android density buckets: [folder, size]
|
||||
const DENSITIES = [
|
||||
['mipmap-mdpi', 48],
|
||||
['mipmap-hdpi', 72],
|
||||
['mipmap-xhdpi', 96],
|
||||
['mipmap-xxhdpi', 144],
|
||||
['mipmap-xxxhdpi', 192],
|
||||
];
|
||||
|
||||
async function generate() {
|
||||
if (!fs.existsSync(LOGO)) {
|
||||
console.error(`ERROR: Logo not found at ${LOGO}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`Generating icons from ${LOGO}...`);
|
||||
|
||||
for (const [folder, size] of DENSITIES) {
|
||||
const dir = path.join(RES, folder);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
|
||||
const png = await sharp(LOGO)
|
||||
.resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||
.png()
|
||||
.toBuffer();
|
||||
|
||||
// Write both legacy and adaptive icon names
|
||||
for (const name of ['ic_launcher.png', 'ic_launcher_foreground.png', 'ic_launcher_round.png']) {
|
||||
fs.writeFileSync(path.join(dir, name), png);
|
||||
}
|
||||
console.log(` ${folder}: ${size}x${size} OK`);
|
||||
}
|
||||
|
||||
// Also write the legacy icon to drawable for completeness
|
||||
const drawableDir = path.join(RES, 'drawable');
|
||||
if (!fs.existsSync(drawableDir)) fs.mkdirSync(drawableDir, { recursive: true });
|
||||
const refPng = await sharp(LOGO)
|
||||
.resize(144, 144, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||
.png()
|
||||
.toBuffer();
|
||||
fs.writeFileSync(path.join(drawableDir, 'ic_launcher.png'), refPng);
|
||||
|
||||
console.log('Done. Android launcher icons generated.');
|
||||
}
|
||||
|
||||
generate().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -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
|
||||
# ── 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"
|
||||
else
|
||||
echo "CAMERA permission already present."
|
||||
fi
|
||||
|
||||
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."
|
||||
# ── 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");
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"beforeDevCommand": "cd ../web && bun run dev"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": false,
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "ZeaVis Edu",
|
||||
@@ -26,5 +26,13 @@
|
||||
"active": true,
|
||||
"targets": "all"
|
||||
},
|
||||
"plugins": {}
|
||||
"plugins": {
|
||||
"deep-link": {
|
||||
"mobile": [
|
||||
{
|
||||
"scheme": ["zeavisedu"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>ZeaVis Edu</title>
|
||||
<script>
|
||||
var LIVE = 'https://zeavisedu.asepharyana.my.id';
|
||||
var T = window.__TAURI_INTERNALS__;
|
||||
|
||||
function navigate(path) {
|
||||
window.location.replace(LIVE + path);
|
||||
}
|
||||
|
||||
// On cold start, check if the app was opened via a deep link (Google OAuth)
|
||||
// before redirecting to the live web app.
|
||||
if (T && T.invoke) {
|
||||
T.invoke('plugin:deep-link|get_current')
|
||||
.then(function(urls) {
|
||||
if (urls && urls.length > 0 && urls[0]) {
|
||||
try {
|
||||
var u = new URL(urls[0]);
|
||||
var target = u.pathname + u.search + u.hash;
|
||||
if (target && target !== '/') {
|
||||
// Preserve full path + query (e.g. /login?token=xxx)
|
||||
navigate(target);
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* malformed URL — fall through */ }
|
||||
}
|
||||
// No deep link — redirect to live app home
|
||||
navigate('/');
|
||||
})
|
||||
.catch(function() { navigate('/'); });
|
||||
} else {
|
||||
// Not in Tauri (dev mode or unknown) — redirect to live
|
||||
navigate('/');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body style="background:#f0fdf4;font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0">
|
||||
<p style="color:#16a34a">Memuat ZeaVis Edu...</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,6 +12,8 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tanstack/react-query": "^5.100.11",
|
||||
"@tauri-apps/plugin-deep-link": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"@zeavis/shared": "workspace:*",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
@@ -21,6 +21,7 @@ import { MainLayout } from "@/components/layout/main-layout";
|
||||
import { useEffect } from "react";
|
||||
import { useAuthStore } from "@/store/auth-store";
|
||||
import { apiClient } from "@/lib/api-client";
|
||||
import { setupDeepLinkHandler } from "@/lib/tauri";
|
||||
|
||||
function LogoutProses() {
|
||||
const setUser = useAuthStore((state) => state.setUser);
|
||||
@@ -161,6 +162,11 @@ function GlobalErrorTracker() {
|
||||
}
|
||||
|
||||
export function App() {
|
||||
// Register deep link handler for Android OAuth return
|
||||
useEffect(() => {
|
||||
setupDeepLinkHandler();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthInitializer />
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 103 KiB |
@@ -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,13 +26,23 @@ 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>
|
||||
<CardTitle>{mode === 'login' ? 'Masuk ke ZeaVis Edu' : 'Buat akun ZeaVis Edu'}</CardTitle>
|
||||
<CardDescription>
|
||||
<Card className="mx-auto w-full max-w-md bg-transparent border-none shadow-none">
|
||||
<CardHeader className="text-center space-y-2">
|
||||
<CardTitle className="text-2xl font-bold text-emerald-900">{mode === 'login' ? 'Masuk Akun ZeaVis Edu' : 'Buat akun ZeaVis Edu'}</CardTitle>
|
||||
<CardDescription className="text-sm text-emerald-800/80">
|
||||
{mode === 'login'
|
||||
? 'Masuk untuk melihat riwayat diagnosis daun jagung Anda.'
|
||||
? 'Masuk untuk menyimpan diagnosis dan mengikuti review pakar.'
|
||||
: 'Daftar untuk menyimpan diagnosis dan mengikuti review pakar.'}
|
||||
</CardDescription>
|
||||
</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,77 @@
|
||||
/**
|
||||
* Lightweight Tauri environment detection and utilities.
|
||||
* Uses raw __TAURI_INTERNALS__ IPC to avoid bundling/import issues on Android.
|
||||
*/
|
||||
|
||||
let _isTauri: boolean | null = null;
|
||||
|
||||
export function isTauri(): boolean {
|
||||
if (_isTauri !== null) return _isTauri;
|
||||
_isTauri =
|
||||
typeof window !== 'undefined' &&
|
||||
'__TAURI_INTERNALS__' in window;
|
||||
return _isTauri;
|
||||
}
|
||||
|
||||
/** Get the Tauri IPC invoke function directly from the global internals. */
|
||||
function tauriInvoke(): (cmd: string, args?: Record<string, unknown>) => Promise<unknown> {
|
||||
const T = (window as any).__TAURI_INTERNALS__;
|
||||
if (!T?.invoke) throw new Error('Tauri IPC not available');
|
||||
return T.invoke.bind(T);
|
||||
}
|
||||
|
||||
export async function openUrl(url: string): Promise<void> {
|
||||
if (!isTauri()) {
|
||||
// Not in Tauri — normal browser navigation
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const invoke = tauriInvoke();
|
||||
await invoke('plugin:opener|open_url', { url });
|
||||
} catch (err) {
|
||||
console.error('Tauri openUrl failed, trying fallback:', err);
|
||||
// Fallback: navigate the WebView (Google will block, but best effort)
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
function processDeepLinkUrl(url: string): void {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const target = u.pathname + u.search + u.hash;
|
||||
if (target && target !== '/') {
|
||||
window.location.href = target;
|
||||
return;
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
|
||||
// Fallback: handle both :// and :/ custom schemes
|
||||
let match = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
|
||||
if (!match) match = url.match(/^[^:]+:\/(\/.*)?$/);
|
||||
if (match?.[1]) window.location.href = match[1];
|
||||
}
|
||||
|
||||
export async function setupDeepLinkHandler(): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
|
||||
try {
|
||||
const invoke = tauriInvoke();
|
||||
|
||||
// Cold-start: app just opened via intent:// or custom scheme
|
||||
invoke('plugin:deep-link|get_current')
|
||||
.then((urls: any) => {
|
||||
if (urls?.[0]) processDeepLinkUrl(urls[0]);
|
||||
})
|
||||
.catch(() => { /* plugin may not be registered yet */ });
|
||||
|
||||
// Warm-start: listen for new URLs while app is running
|
||||
const { listen } = await import('@tauri-apps/api/event');
|
||||
listen('deep-link://new-url', (event: any) => {
|
||||
const urls = event.payload as string[];
|
||||
for (const url of urls) processDeepLinkUrl(url);
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Tauri deep-link setup failed:', err);
|
||||
}
|
||||
}
|
||||
@@ -54,12 +54,12 @@ export function CatalogPage() {
|
||||
<div className="space-y-6 max-w-5xl mx-auto pb-10">
|
||||
{/* Page Title */}
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-extrabold text-[#214B11]">
|
||||
<h1 className="text-2xl font-bold text-emerald-800">
|
||||
Pustaka Penyakit
|
||||
</h1>
|
||||
<p className="mt-1 text-muted-foreground text-sm md:text-base">
|
||||
<p className="text-gray-500 mt-1 text-md">
|
||||
Referensi lengkap penyakit dan kondisi daun jagung yang dapat
|
||||
dideteksi oleh sistem AI ZeaVis Edu.
|
||||
dideteksi oleh sistem AI ZeaVis Edu
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useUiStore } from "@/store/ui-store";
|
||||
import { apiClient } from "@/lib/api-client";
|
||||
import bg from "@/assets/images/dashboard-bg.webp";
|
||||
|
||||
export function DashboardPage() {
|
||||
const { dashboardCompact } = useUiStore();
|
||||
@@ -23,16 +22,17 @@ export function DashboardPage() {
|
||||
const summary = summaryQuery.data;
|
||||
const diseases = diseasesQuery.data ?? [];
|
||||
|
||||
const diseasesQuick = useMemo(() =>
|
||||
diseases
|
||||
.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
.map((d) => ({
|
||||
name: d.commonName,
|
||||
sci: d.label,
|
||||
color: d.accentColor,
|
||||
slug: d.slug,
|
||||
})),
|
||||
[diseases]
|
||||
const diseasesQuick = useMemo(
|
||||
() =>
|
||||
diseases
|
||||
.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||
.map((d) => ({
|
||||
name: d.commonName,
|
||||
sci: d.label,
|
||||
color: d.accentColor,
|
||||
slug: d.slug,
|
||||
})),
|
||||
[diseases],
|
||||
);
|
||||
|
||||
const missionCards = [
|
||||
@@ -74,7 +74,11 @@ export function DashboardPage() {
|
||||
{/* Hero header */}
|
||||
<header
|
||||
className="relative overflow-hidden rounded-3xl bg-cover bg-center bg-no-repeat shadow-sm"
|
||||
style={{ backgroundImage: `url(${bg})` }}
|
||||
style={{
|
||||
backgroundImage: `url(https://cdn.pixabay.com/photo/2014/09/09/19/07/corn-field-440338_1280.jpg)`,
|
||||
backgroundPosition: "bottom",
|
||||
backgroundSize: "cover",
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-[#2F6E1A]/60 to-black/30" />
|
||||
<div className="relative z-10 flex flex-col md:flex-row items-center justify-between gap-6 p-6 md:p-10">
|
||||
@@ -82,7 +86,9 @@ export function DashboardPage() {
|
||||
<span className="inline-block rounded-full bg-[#1E8A2A]/80 px-4 py-2 text-xs font-semibold">
|
||||
AI FOR SMART EDUCATION
|
||||
</span>
|
||||
<h1 className="text-2xl md:text-4xl font-extrabold">Selamat Datang di</h1>
|
||||
<h1 className="text-2xl md:text-4xl font-extrabold">
|
||||
Selamat Datang di
|
||||
</h1>
|
||||
<h2 className="text-2xl md:text-4xl font-extrabold tracking-tight text-[#9AD872]">
|
||||
ZeaVis Edu
|
||||
</h2>
|
||||
@@ -107,10 +113,7 @@ export function DashboardPage() {
|
||||
variant="outline"
|
||||
className="px-4 md:px-6 py-3 md:py-6 text-sm md:text-lg font-semibold text-white hover:bg-[#1E8A2A]"
|
||||
>
|
||||
<Link
|
||||
to="/catalog"
|
||||
className="inline-flex items-center gap-2"
|
||||
>
|
||||
<Link to="/catalog" className="inline-flex items-center gap-2">
|
||||
Pustaka Penyakit
|
||||
<ChevronRight className="h-5 w-6" />
|
||||
</Link>
|
||||
@@ -169,7 +172,10 @@ export function DashboardPage() {
|
||||
<div className="text-3xl font-bold">
|
||||
{summary.imageClassificationCount}
|
||||
</div>
|
||||
<Link to="/diagnoses" className="text-emerald-600 ml-auto hover:underline">
|
||||
<Link
|
||||
to="/diagnoses"
|
||||
className="text-emerald-600 ml-auto hover:underline"
|
||||
>
|
||||
Lihat daftar
|
||||
</Link>
|
||||
</CardContent>
|
||||
@@ -186,7 +192,10 @@ export function DashboardPage() {
|
||||
<div className="text-3xl font-bold text-amber-600">
|
||||
{summary.needsReviewCount}
|
||||
</div>
|
||||
<Link to="/diagnoses?status=needs_review" className="text-amber-600 ml-auto hover:underline">
|
||||
<Link
|
||||
to="/diagnoses?status=needs_review"
|
||||
className="text-amber-600 ml-auto hover:underline"
|
||||
>
|
||||
Lihat daftar
|
||||
</Link>
|
||||
</CardContent>
|
||||
@@ -200,10 +209,11 @@ export function DashboardPage() {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="h-full flex flex-col justify-start pt-2">
|
||||
<div className="text-3xl font-bold text-red-600">
|
||||
—
|
||||
</div>
|
||||
<Link to="/diagnoses?status=failed" className="text-red-600 ml-auto hover:underline">
|
||||
<div className="text-3xl font-bold text-red-600">—</div>
|
||||
<Link
|
||||
to="/diagnoses?status=failed"
|
||||
className="text-red-600 ml-auto hover:underline"
|
||||
>
|
||||
Lihat daftar
|
||||
</Link>
|
||||
</CardContent>
|
||||
@@ -220,7 +230,10 @@ export function DashboardPage() {
|
||||
<div className="text-3xl font-bold text-red-600">
|
||||
{summary.riskDistribution.high}
|
||||
</div>
|
||||
<Link to="/catalog?risk=high" className="text-red-600 ml-auto hover:underline">
|
||||
<Link
|
||||
to="/catalog?risk=high"
|
||||
className="text-red-600 ml-auto hover:underline"
|
||||
>
|
||||
Lihat pustaka
|
||||
</Link>
|
||||
</CardContent>
|
||||
@@ -276,7 +289,8 @@ export function DashboardPage() {
|
||||
Penyakit yang Dapat Dideteksi
|
||||
</h3>
|
||||
<p className="text-[15px] font-normal text-muted-foreground">
|
||||
{diseases.length} kelas penyakit dan kondisi daun jagung dalam sistem kami
|
||||
{diseases.length} kelas penyakit dan kondisi daun jagung dalam
|
||||
sistem kami
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
@@ -295,9 +309,7 @@ export function DashboardPage() {
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{diseasesQuick.map((d) => (
|
||||
<Link key={d.slug} to={`/catalog/${d.slug}`}>
|
||||
<Card
|
||||
className="rounded-2xl bg-white p-4 shadow-sm h-full transition-transform hover:scale-[1.03] hover:shadow-md cursor-pointer"
|
||||
>
|
||||
<Card className="rounded-2xl bg-white p-4 shadow-sm h-full transition-transform hover:scale-[1.03] hover:shadow-md cursor-pointer">
|
||||
<CardContent className="h-full p-4 flex flex-col justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
@@ -324,7 +336,9 @@ export function DashboardPage() {
|
||||
{/* Scan quick access */}
|
||||
<div className="mt-12 flex flex-col sm:flex-row items-center gap-4 sm:gap-6 bg-[#1E8A2A] rounded-3xl p-5 sm:p-6">
|
||||
<div className="flex-1 text-white">
|
||||
<h3 className="text-2xl font-bold">Siap Mendeteksi Penyakit Daun?</h3>
|
||||
<h3 className="text-2xl font-bold">
|
||||
Siap Mendeteksi Penyakit Daun?
|
||||
</h3>
|
||||
<p className="text-sm text-[#9AD872] font-normal mt-2">
|
||||
Unggah foto daun jagung Anda dan dapatkan hasil analisis AI
|
||||
dalam hitungan detik.
|
||||
|
||||
@@ -75,7 +75,7 @@ export function DiagnosesPage() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-emerald-800">Diagnosa Tanaman</h1>
|
||||
<h1 className="text-2xl font-bold text-emerald-800">Diagnosa Penyakit</h1>
|
||||
<p className="text-gray-500 mt-1 text-md">
|
||||
Lihat hasil diagnosa dari scan yang telah dilakukan
|
||||
</p>
|
||||
|
||||
@@ -71,7 +71,7 @@ export function ExpertReviewsPage() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-emerald-800">Review Pakar</h1>
|
||||
<h1 className="text-2xl font-bold text-emerald-800">Tinjauan Pakar</h1>
|
||||
<p className="text-gray-500 mt-1 text-md">
|
||||
Tinjau hasil diagnosa dari scan yang telah dilakukan dan berikan
|
||||
feedback untuk meningkatkan akurasi sistem AI ZeaVis Edu
|
||||
|
||||
@@ -65,15 +65,29 @@ export function LoginPage() {
|
||||
<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>
|
||||
<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">
|
||||
<main className="relative flex min-h-screen items-center justify-center px-6 py-12">
|
||||
{/* Background Image with Overlay */}
|
||||
<div
|
||||
className="absolute inset-0 z-0"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), url(https://cdn.pixabay.com/photo/2014/09/09/19/07/corn-field-440338_1280.jpg)`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Glassmorphism Container */}
|
||||
<div className="relative z-10 w-full max-w-sm md:max-w-md space-y-6 bg-white/60 backdrop-blur-md p-8 md:p-10 rounded-3xl shadow-2xl border border-white/50">
|
||||
<AuthForm
|
||||
mode="login"
|
||||
isSubmitting={mutation.isPending}
|
||||
@@ -87,9 +101,13 @@ export function LoginPage() {
|
||||
}}
|
||||
onFieldChange={() => setError(null)}
|
||||
/>
|
||||
<p className="text-center text-sm text-muted-foreground">
|
||||
|
||||
<p className="text-center text-sm text-slate-700">
|
||||
Belum punya akun?{" "}
|
||||
<Link className="text-primary" to="/register">
|
||||
<Link
|
||||
className="text-emerald-700 font-bold hover:underline"
|
||||
to="/register"
|
||||
>
|
||||
Daftar
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
@@ -28,7 +28,18 @@ export function RegisterPage() {
|
||||
|
||||
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">
|
||||
{/* Background Image with Overlay */}
|
||||
<div
|
||||
className="absolute inset-0 z-0"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), url(https://cdn.pixabay.com/photo/2014/09/09/19/07/corn-field-440338_1280.jpg)`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
/>
|
||||
{/* Glassmorphism Container */}
|
||||
<div className="relative z-10 w-full max-w-sm md:max-w-md space-y-6 bg-white/60 backdrop-blur-md p-8 md:p-10 rounded-3xl shadow-2xl border border-white/50">
|
||||
<AuthForm
|
||||
mode="register"
|
||||
isSubmitting={mutation.isPending}
|
||||
|
||||
@@ -36,21 +36,18 @@ export function ScanPage() {
|
||||
|
||||
// Camera mode state
|
||||
const [useCamera, setUseCamera] = useState(false);
|
||||
const handleCameraCapture = useCallback(
|
||||
(file: File) => {
|
||||
setFileName(file.name);
|
||||
const url = URL.createObjectURL(file);
|
||||
setPreviewUrl(url);
|
||||
const handleCameraCapture = useCallback((file: File) => {
|
||||
setFileName(file.name);
|
||||
const url = URL.createObjectURL(file);
|
||||
setPreviewUrl(url);
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
setImageDimensions({ width: img.width, height: img.height });
|
||||
};
|
||||
img.src = url;
|
||||
setUseCamera(false);
|
||||
},
|
||||
[],
|
||||
);
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
setImageDimensions({ width: img.width, height: img.height });
|
||||
};
|
||||
img.src = url;
|
||||
setUseCamera(false);
|
||||
}, []);
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (file: File) => apiClient.createDiagnosis(file),
|
||||
@@ -107,10 +104,10 @@ export function ScanPage() {
|
||||
{/* Main Header */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-emerald-800">Scan Tanaman</h1>
|
||||
<h1 className="text-2xl font-bold text-emerald-800">Pindai Daun</h1>
|
||||
<p className="text-gray-500 mt-1 text-md">
|
||||
Unggah foto daun jagung untuk dianalisis oleh sistem AI kami secara
|
||||
real-time.
|
||||
real-time
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild variant="outline">
|
||||
@@ -170,12 +167,16 @@ export function ScanPage() {
|
||||
className="w-full border-2 border-dashed border-green-300 rounded-md p-10 h-60 text-center cursor-pointer"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
<Upload className="mx-auto text-green-500 mb-3" size={48} />
|
||||
<Upload
|
||||
className="mx-auto text-green-500 mb-3"
|
||||
size={48}
|
||||
/>
|
||||
<h3 className="font-semibold text-base text-gray-800 mb-1">
|
||||
Seret & Lepas Foto Daun
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mb-3">
|
||||
atau klik untuk memilih file berkas dari perangkat Anda
|
||||
atau klik untuk memilih file berkas dari perangkat
|
||||
Anda
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 justify-center">
|
||||
<div className="bg-green-100 text-green-700 px-3 py-1 rounded-full inline-flex items-center gap-1 text-xs font-medium">
|
||||
|
||||
@@ -286,12 +286,11 @@ export function TelemetryPage() {
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-[28px] font-extrabold text-[#214B11] flex items-center gap-3">
|
||||
<Activity className="h-7 w-7 text-[#48A111]" />
|
||||
<h1 className="text-2xl font-bold text-emerald-800">
|
||||
Telemetry Dashboard
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground mt-0.5">
|
||||
Real-time metrics from Prometheus
|
||||
<p className="text-gray-500 mt-1 text-md">
|
||||
Real-time monitoring dari performa sistem dan aplikasi ZeaVis Edu
|
||||
{error && <span className="text-amber-600 ml-2">(partial — {error})</span>}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+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,11 @@
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "zeavis-edu",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "2.11.0",
|
||||
"@tauri-apps/plugin-deep-link": "2.4.9",
|
||||
"@tauri-apps/plugin-opener": "2.5.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@moonrepo/cli": "^2.2.5",
|
||||
"typescript": "^6.0.3",
|
||||
@@ -36,6 +41,9 @@
|
||||
"apps/tauri": {
|
||||
"name": "@zeavis/tauri",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"sharp": "0.35.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2",
|
||||
},
|
||||
@@ -46,6 +54,8 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tanstack/react-query": "^5.100.11",
|
||||
"@tauri-apps/plugin-deep-link": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"@zeavis/shared": "workspace:*",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -89,7 +99,7 @@
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||
|
||||
@@ -153,6 +163,60 @@
|
||||
|
||||
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-T15JRWOubQ3f5+GxnWeIvo47u5qV0M9HBgJhT+f2gE1e9e6OhR6K73Re52Hm80qWcu1DNb3GweKmpr/MnuP2Ow=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.0" }, "os": "darwin", "cpu": "x64" }, "sha512-t1CPD0cr7XCHjwUj6tQ5MC0pCi866I+gUW6zbUX4aFPnKd1DFBtk0M+gWcjX8VeEzgfCNiSiNTVFZ6b7kvdbnQ=="],
|
||||
|
||||
"@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.1", "", { "dependencies": { "@img/sharp-wasm32": "0.35.1" }, "os": "freebsd" }, "sha512-MBSQXqNPThW9EcZ905H6N4sEdX5EwZEYzGx5EBq9ncDCGJALMiY1xPFJxNdzuB1iBjLOpIfxajM6YxdvwmQSLA=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A=="],
|
||||
|
||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.0", "", { "os": "linux", "cpu": "none" }, "sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.0" }, "os": "linux", "cpu": "arm" }, "sha512-jygmR02PpCYypt7xB7nst1vqjZp/BpRA/Kf9nK7qRponJ/KrLPaZWEG4G15z1d2FZ6XqI+T0350ha3RSnKx24A=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.0" }, "os": "linux", "cpu": "arm64" }, "sha512-ErCRyGU7LeoaFBZ0xW8hhLlXzhAg80sc4vxePB86qvtEvW1jEhhmbiNBP4oEzZfPMnu6HwHXfzD2W2kBU+RnCw=="],
|
||||
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.0" }, "os": "linux", "cpu": "ppc64" }, "sha512-LUWZ2+r2UoLCd8j0RLCwQ4gL6w47+Y7igxtVnPIDXOOEjV86LpBkAHq5VpJeg+GHbw0KN/JWlPJOdZjyZnFqFQ=="],
|
||||
|
||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.0" }, "os": "linux", "cpu": "none" }, "sha512-i7x6J3mwF4JgT0sM4V4WlAWdJ0bucPtA9rzO1bTji1n5qgBq/W5nn87RvOQPleuuxahNoLdTngByD8/vDDLArw=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.0" }, "os": "linux", "cpu": "s390x" }, "sha512-0zSaTUjTF0kIWTSYxD4EG/nvCU4jez53+3RdURtoY3HvbXtIQ98W90JnrGz/oLRFuEnfIy9+7xeq883euc0ZWw=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.0" }, "os": "linux", "cpu": "x64" }, "sha512-NbJD4mWdeyrNQKluO/tR/wBDOelcowSVGNBWxI0e3ZtlXc6F/UOVKDj1MLD4zl3oHTuvKW3s+MA9N54YTldAYw=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.0" }, "os": "linux", "cpu": "arm64" }, "sha512-VoW2sQCWI+0YIKQEmWJ8vzaQjTg9wIyfkFpvEfAS2h43X6iHu7GTk1hhOgB4IpSzCHe8UwQZIcx7b81VTaOrJA=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.0" }, "os": "linux", "cpu": "x64" }, "sha512-LjBoSd/c5JU0/K5MwzDMlgsSRP2bPn98JQGFFQAOLQ0bU/1z4ekxUdSKY9BmlwSh/cA+OrvpgsWqfZyYfVHBRw=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.1", "", { "dependencies": { "@emnapi/runtime": "^1.11.0" } }, "sha512-PCQUoQdZyE8tp3HpbevuihfUmgSP4qWI0FGEPWoeXqaS+cUrFfemabHQiebUmUmlUhCuNnQMxGrQ+CPqK4hnxg=="],
|
||||
|
||||
"@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.1", "", { "dependencies": { "@img/sharp-wasm32": "0.35.1" }, "cpu": "none" }, "sha512-xU2ml2bU2OPxYVvW2A6ae4M1g5QKyhKG06P4FAt+YEaFQQO0919Qx+XxIZEUuWTMoDViLpMws2/dQwoe/VcA6A=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-IkmHwuFhYpd3bTsN5SAahjwhiAcyXPooBt8vEUgxY3T0IP70sSJ0nU1xiPzZY8AH/OB1XpV3j8aZSVSOSfTbdA=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-wQahqCi9MD8Yxzg4gVM4fNrZxh+r6vD55PyIg+WJPaM5ZRUyF35iQpwJCuma3r6viU9/8Pxlc+XHV+woVa6nCQ=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.1", "", { "os": "win32", "cpu": "x64" }, "sha512-WzBtkYtZHATLPe8XRharxZXxQ9cdLrQWHiwxt+BJ5rBsisQrKeeV86ErxPSVhcG6xCEuNhs0SqLpWr7XDa2k6w=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||
@@ -341,6 +405,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 +431,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=="],
|
||||
@@ -605,8 +675,12 @@
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
||||
|
||||
"semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="],
|
||||
|
||||
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
|
||||
|
||||
"sharp": ["sharp@0.35.1", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.1", "@img/sharp-darwin-x64": "0.35.1", "@img/sharp-freebsd-wasm32": "0.35.1", "@img/sharp-libvips-darwin-arm64": "1.3.0", "@img/sharp-libvips-darwin-x64": "1.3.0", "@img/sharp-libvips-linux-arm": "1.3.0", "@img/sharp-libvips-linux-arm64": "1.3.0", "@img/sharp-libvips-linux-ppc64": "1.3.0", "@img/sharp-libvips-linux-riscv64": "1.3.0", "@img/sharp-libvips-linux-s390x": "1.3.0", "@img/sharp-libvips-linux-x64": "1.3.0", "@img/sharp-libvips-linuxmusl-arm64": "1.3.0", "@img/sharp-libvips-linuxmusl-x64": "1.3.0", "@img/sharp-linux-arm": "0.35.1", "@img/sharp-linux-arm64": "0.35.1", "@img/sharp-linux-ppc64": "0.35.1", "@img/sharp-linux-riscv64": "0.35.1", "@img/sharp-linux-s390x": "0.35.1", "@img/sharp-linux-x64": "0.35.1", "@img/sharp-linuxmusl-arm64": "0.35.1", "@img/sharp-linuxmusl-x64": "0.35.1", "@img/sharp-webcontainers-wasm32": "0.35.1", "@img/sharp-win32-arm64": "0.35.1", "@img/sharp-win32-ia32": "0.35.1", "@img/sharp-win32-x64": "0.35.1" } }, "sha512-lW979AMi+ESidzMv/Lnv+F9bknzLyxLqFI05Sm433vOeRcltgxQmXpnfOOFIAlKtwXU/ksupm2srQoFCkR214g=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
@@ -671,6 +745,8 @@
|
||||
|
||||
"@reduxjs/toolkit/immer": ["immer@11.1.8", "", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="],
|
||||
|
||||
"@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
|
||||
|
||||
+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)
|
||||
|
||||
+6
-1
@@ -13,5 +13,10 @@
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
]
|
||||
],
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "2.11.0",
|
||||
"@tauri-apps/plugin-deep-link": "2.4.9",
|
||||
"@tauri-apps/plugin-opener": "2.5.4"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user