Compare commits

..
30 Commits
Author SHA1 Message Date
MythEclipseandClaude 4b252a1d5d fix(tauri): enable withGlobalTauri so Tauri APIs work on live web
Without this, the redirect page loads zeavisedu.asepharyana.my.id but
__TAURI_INTERNALS__ is not injected, so isTauri() returns false,
handleGoogleLogin uses window.location.href (WebView navigation), and
Google blocks embedded WebView since 2016.

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 02:58:52 +07:00
Selly SupriyatinandGitHub 3b5839ebd5 Merge pull request #42 from ATLAS-PJK-GM007/selly/frontend
style(sidebar): standardize capitalization of feature titles
2026-06-16 02:48:40 +07:00
MythEclipseandClaude e2742a7a60 fix(auth): fix Android deep link intent URL format for Google OAuth
The intent:// URL in renderTauriDeepLinkPage was intent:/path (single slash),
producing zeavisedu:/login?token=xxx — a non-hierarchical URL that new URL()
cannot parse. Fixed to intent://login/path which produces a proper
hierarchical URI (zeavisedu://login/login?token=xxx).

Also hardened setupDeepLinkHandler to handle both double-slash (://) and
single-slash (:/) custom-scheme URLs as fallback.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 02:44:33 +07:00
seriouselly ae22adee40 style(sidebar): standardize capitalization of feature titles
- Update navigation labels in the sidebar for consistent casing and better visual uniformity.
2026-06-16 02:38:01 +07:00
Selly SupriyatinandGitHub ee02bd4f9a Merge pull request #41 from ATLAS-PJK-GM007/selly/frontend
Selly/frontend
2026-06-16 02:14:36 +07:00
seriouselly 310d8adc8d Merge branch 'main' of https://github.com/ATLAS-PJK-GM007/ZeaVis-Edu into selly/frontend 2026-06-16 02:13:32 +07:00
seriouselly ccbcd99053 refactor(ui): remove local image assets and use external URLs
- Delete the `assets/images` directory to reduce repository size and footprint.
- Update `dashboard-page.tsx` to fetch background images from external URLs instead of local file imports.
2026-06-16 02:13:17 +07:00
seriouselly 52e60b99dd Revert "refactor(ui): remove local image assets and use external URLs"
This reverts commit e19321a2b7.
2026-06-16 02:10:32 +07:00
seriouselly e19321a2b7 refactor(ui): remove local image assets and use external URLs
- Delete the `assets/images` directory to reduce repository size and footprint.
- Update `dashboard-page.tsx` to fetch background images from external URLs instead of local file imports.
2026-06-16 02:09:36 +07:00
MythEclipse d1275b5b65 fix(tauri): remove unused setup closure entirely 2026-06-16 02:04:07 +07:00
MythEclipse 12162fd6f7 fix(tauri): remove setup block that caused E0599 compile error
app.get_webview_window() needs tauri::Manager trait imported.
Removed the live URL redirect in setup for now — the bundled
frontend works fine. Can re-add later with proper imports.
2026-06-16 02:03:38 +07:00
MythEclipseandClaude 9ab8d41adf feat(android): generate launcher icons from zeavis-logo.svg during CI build
- Add scripts/generate-icons.js using sharp to convert SVG to PNG
  at all Android density buckets (mdpi 48→xxxhdpi 192)
- Add sharp as devDependency
- Update CI workflow to run icon generation after tauri android init
- Replaces Tauri's default icons with ZeaVis Edu logo

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 01:53:59 +07:00
Selly SupriyatinandGitHub 6f07fc05ac Merge pull request #40 from ATLAS-PJK-GM007/selly/frontend
Selly/frontend
2026-06-16 01:53:39 +07:00
seriouselly cbcc7da33d Merge branch 'main' of https://github.com/ATLAS-PJK-GM007/ZeaVis-Edu into selly/frontend 2026-06-16 01:52:02 +07:00
seriouselly d1282364e7 refactor(auth): replace local background images with external URLs
- Remove local image imports for the background in login and register pages.
- Update background-image styles to use external URL links for easier asset management.
2026-06-16 01:51:22 +07:00
MythEclipseandClaude 254fe19bb6 feat(android): always load live web URL so APK never needs rebuild for web changes
On Android, the app navigates to the live production URL immediately
after setup. The APK becomes a thin shell — bundled frontend is only
a placeholder for the ~1 second before redirect. All web updates
(deploy web) take effect instantly on all installed APKs.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 01:49:03 +07:00
MythEclipseandClaude 99acc43ff0 fix(android): use intent:// protocol for deep link return from system browser
Replace custom zeavisedu:// scheme in callback HTML with Chrome's
native intent:// protocol which directly opens the target Android app
by package name. Includes browser_fallback_url for non-app scenarios.

Also updates the HTML page with better UX: auto-redirect via JS,
fallback button, and copyable URL for manual paste.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 01:25:02 +07:00
MythEclipseandClaude 1afd2e013c fix(android): add deep link handler to intercept zeavisedu:// URLs and navigate WebView
Login-page won't auto-process token on deep link return because
the WebView stays on the page it was on. Added setupDeepLinkHandler()
which listens for zeavisedu:// scheme URLs and navigates the WebView
to the correct path+query.

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

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-15 23:49:47 +07:00
MythEclipseandClaude d292bc4b3c feat(auth): add Google logo SVG to 'Masuk dengan Google' button
Inline SVG with official Google brand colors (blue, green, yellow, red)
placed before the label text with gap-2.5 spacing.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-15 23:17:46 +07:00
MythEclipseandClaude 787acf077f fix(vite): add data-cfasync="false" to script tags to prevent Cloudflare Rocket Loader breaking JS
Cloudflare Rocket Loader rewrites <script type="module"> to
<script type="randomhash-module"> which browsers can't parse,
causing complete blank page. data-cfasync="false" disables this.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-15 23:00:30 +07:00
MythEclipseandClaude 250355562f fix(login): replace useSearchParams with native URLSearchParams + loading spinner
- useSearchParams can lose params during re-renders, causing blank page
- Use native window.location.search + URLSearchParams instead (always accessible)
- Add oauthProcessing spinner state so user sees 'Menyelesaikan login...'
  instead of blank page while /auth/me is being called

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-15 22:38:11 +07:00
MythEclipseandClaude d47c73308e fix(auth): replace deprecated set.redirect with manual 302 Location header
Elysia's set.redirect returns 200 OK instead of 302 redirect on the
current version. Use set.status = 302 + set.headers['Location'] instead
for both /auth/google (Google OAuth redirect) and /auth/google/callback
(all redirect paths: errors, success token delivery).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-15 22:06:59 +07:00
MythEclipseandClaude 4b1d70d1c4 ci(deploy): inject Google OAuth env vars into VPS .env
Add GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and GOOGLE_REDIRECT_URI
to the deploy workflow so the API can use Google OAuth on production.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-15 21:57:12 +07:00
MythEclipseandClaude c9ae90a042 feat(auth): implement Google OAuth callback with user auto-creation
API (apps/api/src/routes/auth.ts):
- Exchange authorization code for tokens via Google's token endpoint
- Decode id_token payload (JWT) to extract sub, email, name
- Find user by googleId → fall back to email match (link accounts)
- Auto-create user if neither found (role: 'user', no password)
- Create session + set cookie, redirect to /login?token=<token>

Web (apps/web/src/pages/login-page.tsx):
- Consume ?token= query param from OAuth redirect
- Store token in localStorage for future API calls
- Fetch /auth/me to hydrate Zustand store, then navigate to dashboard
- Show OAuth errors from ?error= query param

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

After

Width:  |  Height:  |  Size: 1.1 KiB

+5 -1
View File
@@ -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
+3
View File
@@ -179,6 +179,9 @@ jobs:
SESSION_SECRET=${{ secrets.SESSION_SECRET }}
WEB_APP_URL=https://zeavisedu.asepharyana.my.id
ML_SERVICE_URL=http://zeavis-ml:8000
GOOGLE_CLIENT_ID=${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET=${{ secrets.GOOGLE_CLIENT_SECRET }}
GOOGLE_REDIRECT_URI=https://zeavisedu.asepharyana.my.id/api/v1/auth/google/callback
ENVEOF
} > .env
+9 -3
View File
@@ -1,6 +1,8 @@
# Corn Leaf Disease Classification
# Pipeline Machine Learning — ZeaVis Edu
Pipeline lengkap untuk klasifikasi penyakit daun jagung menggunakan **EfficientNetV2B0**, mulai dari persiapan dataset, pelatihan di Google Colab, hingga ekspor model ke format **TFLite** dan **TensorFlow.js** untuk kebutuhan produksi.
> Panduan lengkap: preprocessing dataset, pelatihan di Google Colab, ekspor model ke TFLite, TensorFlow.js, dan ONNX.
← [Kembali ke README utama](../README.md)
---
@@ -390,4 +392,8 @@ export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
---
### Sesi Colab terputus saat training
**Solusi:** Gunakan callback `ModelCheckpoint` di notebook untuk menyimpan checkpoint secara berkala ke Google Drive, sehingga training bisa dilanjutkan dari checkpoint terakhir tanpa mengulang dari awal.
**Solusi:** Gunakan callback `ModelCheckpoint` di notebook untuk menyimpan checkpoint secara berkala ke Google Drive, sehingga training bisa dilanjutkan dari checkpoint terakhir tanpa mengulang dari awal.
---
← [Kembali ke README utama](../README.md) &bull; [ML Service →](../apps/ml-service/README.md)
+263 -419
View File
@@ -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 &bull; EfficientNetV2B0 &bull; Rust ONNX Runtime &bull; 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> &bull;
<a href="#-tim"><b>Tim</b></a> &bull;
<a href="#-ringkasan-eksekutif"><b>Ringkasan</b></a> &bull;
<a href="#-cakupan--deliverables"><b>Cakupan</b></a> &bull;
<a href="#-jadwal"><b>Jadwal</b></a> &bull;
<a href="#-tech-stack"><b>Tech Stack</b></a> &bull;
<a href="#-memulai"><b>Memulai</b></a> &bull;
<a href="#-platform"><b>Platform</b></a> &bull;
<a href="#-dokumentasi"><b>Dokumentasi</b></a>
</p>
## Fitur Utama
<br>
- Aplikasi web untuk pengalaman pengguna dan interaksi edukatif.
- API backend untuk status layanan, integrasi data, dan komunikasi dengan layanan ML.
- ML service berbasis Rust/Axum dengan ONNX Runtime untuk inferensi penyakit daun jagung dari gambar.
- Pipeline machine learning untuk preprocessing dataset, training di Google Colab, dan ekspor model produksi.
- Dukungan Docker untuk deployment web, API, dan ML service.
- Workspace monorepo berbasis Bun dan Moon untuk menjalankan task development, typecheck, dan build secara terpusat.
---
## Kelas Penyakit
## 🌽 Tentang
Model klasifikasi menargetkan empat label berbahasa Indonesia:
**ZeaVis Edu** adalah aplikasi edukasi berbasis **Computer Vision** yang membantu petani, mahasiswa pertanian, dan penyuluh lapangan mengidentifikasi penyakit daun jagung secara mandiri — cukup dengan mengunggah foto daun jagung.
| Label | Deskripsi |
Proyek ini merupakan **Capstone Project** dalam program **Pijak × IBM SkillsBuild** dengan tema **"AI for Smart Education"**, dirancang untuk menjembatani kesenjangan antara pengetahuan teori pertanian dan kebutuhan praktis di lapangan.
---
## 👥 Tim
| NPM | Nama | Learning Path | Peran |
|---|---|---|---|
| APC246D6Y0028 | **Asep Haryana Saputra** | Back-End | Arsitektur sistem, RESTful API, deployment Docker/Cloud, keamanan upload stream |
| APC013D6X0081 | **Selly Supriyatin** | Front-End | UI/UX responsif, mekanisme unggah gambar, modul edukasi (rekomendasi obat & penanganan) |
| APC013D6Y0091 | **Taufik Pathurrohman** | Machine Learning | Data Engineering — ekstraksi dataset, cleaning, augmentasi gambar |
| APC414D6Y0138 | **Luhung Pandyaska Suyi** | Machine Learning | Model Architecture & Training — CNN, hyperparameter tuning |
| APC013D6Y0269 | **Ardian** | Machine Learning | Model Evaluation & Deployment Prep — confusion matrix, konversi ke production-ready |
---
## 📋 Ringkasan Eksekutif
### Masalah
Data BPS menunjukkan penurunan luas panen jagung dari **2.764.366 Ha (2022)** menjadi **2.487.191 Ha (2023)**. Salah satu penyebab utamanya adalah penyakit daun seperti **Hawar Daun**, **Karat Daun**, dan **Bercak Daun Abu-abu** yang menyebabkan nekrosis dan menghambat fotosintesis.
Petani sering kesulitan mengidentifikasi penyakit secara kasat mata dan memiliki **ketergantungan tinggi pada POPT** (Petugas Pengendali Organisme Pengganggu Tumbuhan) akibat minimnya media pembelajaran interaktif.
### Solusi
ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
1. 📸 **Unggah** foto daun jagung yang diduga terinfeksi
2. 🤖 **Deteksi otomatis** penyakit oleh model AI (EfficientNetV2B0)
3. 📚 **Dapatkan** informasi detail penyakit, panduan pencegahan, dan rekomendasi obat secara mandiri
### Metode Teknis
| Komponen | Pilihan |
|---|---|
| Bercak Daun | Gray Leaf Spot |
| 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** | 1117 Mei 2026 | Inisiasi & Data | Spesifikasi teknis (Asep) • Dataset dari Kaggle + preprocessing (Taufik) • Wireframe UI/UX (Selly) |
| **2** | 1824 Mei 2026 | Training & Dev Awal | Implementasi EfficientNetV2B0 di Colab (Luhung) • Slicing UI ke React (Selly) • Setup server, database, routing API (Asep) |
| **3** | 2531 Mei 2026 | Evaluasi & Modul Edukasi | Evaluasi akurasi + konversi model ke ONNX/TFLite (Ardian) • Halaman edukasi obat & penanganan (Selly) • RESTful API untuk image upload & inferensi (Asep) |
| **4** | 17 Juni 2026 | Integrasi & Testing | Integrasi penuh Front-End ↔ API ↔ Model ML • Pengujian end-to-end • Stress testing & error handling (Semua) |
| **5** | 814 Juni 2026 | Deployment & Finalisasi | Deployment ke VPS (Asep) • Bug fixing & optimalisasi UI/UX (Selly) • Dokumentasi teknis & materi presentasi (Semua) |
---
## ⚠️ Manajemen Risiko
| Risiko | Solusi |
|---|---|
| **Overfitting akibat imbalanced data** | Augmentasi tingkat lanjut (kecerahan, noise, rotasi) + confidence threshold < 75% → minta user foto ulang |
| **Server downtime / latensi tinggi** | Batasan upload ≤ 5 MB + kompresi server-side + rate limiting + container Docker isolasi resource |
| **Foto blur / objek bukan daun jagung** | Panduan visual (overlay) pada UI + validasi anomali + disclaimer "alat bantu edukasi, bukan pengganti POPT" |
| **Bottleneck integrasi ML ↔ API ↔ UI** | API Contract ketat di minggu ke-1 + integrasi bertahap (CI) mulai minggu ke-3 |
---
## 🏗️ Arsitektur Proyek
```
.
├── apps/
│ ├── 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 &bull; Vite &bull; TypeScript &bull; React Router &bull; TanStack Query &bull; Zustand &bull; Tailwind CSS
**Tauri 2** (Android) &bull; Rust &bull; WebView &bull; Deep Link OAuth
### Backend API
- Bun
- Elysia
- Drizzle ORM
- PostgreSQL
Bun &bull; Elysia &bull; Drizzle ORM &bull; PostgreSQL &bull; prom-client
### Machine Learning
Python &bull; TensorFlow/Keras &bull; EfficientNetV2B0 &bull; Google Colab (GPU T4)
- Python (preprocessing, training, export)
- TensorFlow/Keras
- EfficientNetV2B0
- Rust
- Axum
- ONNX Runtime
- TFLite
- TensorFlow.js
### Inference Engine
**Rust** &bull; **Axum** &bull; **ONNX Runtime** &bull; TFLite &bull; TensorFlow.js
### Tooling & Deployment
### DevOps & Infrastruktur
Docker &bull; Docker Compose &bull; Coolify &bull; Traefik &bull; Tailscale &bull; GitHub Actions (CI/CD)
- Bun workspaces
- Moon task runner
- Docker
- Docker Compose
- GitHub Container Registry
- Traefik labels untuk routing deployment
### Observabilitas
Prometheus &bull; Metric Ingester (Go) &bull; Vector &bull; ClickHouse &bull; Query Proxy (Go) &bull; 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.93.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.93.11 untuk pipeline ML
- Rust dan Cargo untuk `apps/ml-service`
- Docker dan Docker Compose jika ingin menjalankan/deploy via container
- PostgreSQL jika fitur backend yang membutuhkan database digunakan
- File model `Machine_Learning/model/model.onnx` untuk inferensi ML lokal
## Instalasi Root Workspace
Jalankan dari root repository:
### 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` &bull; `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>
+191 -4
View File
@@ -18,6 +18,103 @@ import {
import { env } from '../config/env';
import { authCounter } from '../lib/telemetry';
// ── Google OAuth Helpers ──────────────────────────────────────────────
interface GoogleTokenResponse {
access_token: string;
id_token: string;
}
interface GoogleIdPayload {
sub: string;
email: string;
email_verified: boolean;
name: string;
picture?: string;
}
async function exchangeGoogleCode(code: string): Promise<GoogleTokenResponse> {
const res = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
code,
client_id: env.googleClientId!,
client_secret: env.googleClientSecret!,
redirect_uri: env.googleRedirectUri!,
grant_type: 'authorization_code',
}),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`Google token exchange failed: ${res.status} ${err}`);
}
return res.json() as Promise<GoogleTokenResponse>;
}
function decodeGoogleIdToken(idToken: string): GoogleIdPayload {
const parts = idToken.split('.');
if (parts.length !== 3) {
throw new Error('Invalid id_token format');
}
const payload = Buffer.from(parts[1], 'base64url').toString('utf-8');
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, '&quot;');
const escapedPath = pathAndQuery.replace(/"/g, '&quot;');
// 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, '&quot;')}" 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, '&lt;').replace(/>/g, '&gt;')}</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() : '';
}
@@ -123,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!,
@@ -136,15 +236,102 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
scope: 'openid email profile',
access_type: 'offline',
prompt: 'select_account',
state,
});
set.redirect = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
set.status = 302;
set.headers['Location'] = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
})
.get('/google/callback', ({ set }) => {
.get('/google/callback', async ({ query, set, request }) => {
if (!env.googleOAuthEnabled) {
set.status = 404;
return { error: 'Google OAuth is not configured' };
}
set.redirect = `${env.webAppUrl}/login?oauth=not-implemented`;
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'] = url;
return;
}
// Exchange authorization code for tokens
let idPayload: GoogleIdPayload;
try {
const tokens = await exchangeGoogleCode(code);
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'] = 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'] = url;
return;
}
const googleId = idPayload.sub;
const email = idPayload.email.trim().toLowerCase();
const name = idPayload.name?.trim() ?? email.split('@')[0];
try {
const db = createDbClient();
let user = await db.select().from(users).where(eq(users.googleId, googleId)).limit(1).then(r => r[0] ?? null);
if (!user) {
user = await db.select().from(users).where(eq(users.email, email)).limit(1).then(r => r[0] ?? null);
if (user) {
await db.update(users).set({ googleId }).where(eq(users.id, user.id));
}
}
if (!user) {
const inserted = await db
.insert(users)
.values({ email, name, googleId, role: 'user' })
.returning();
user = inserted[0];
authCounter.labels('register', 'true').inc();
}
const token = await createSession(user.id);
const sessionCookie = createSessionCookie(token, request.headers);
authCounter.labels('login', 'true').inc();
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'] = successUrl;
} catch (err) {
const url = makeErrorUrl('Database unavailable');
if (platform === 'tauri') return renderTauriDeepLinkPage(url);
set.status = 302;
set.headers['Location'] = url;
}
});
+97 -163
View File
@@ -1,8 +1,25 @@
# ML Service — Rust Axum ONNX Runtime
# ML Inference Service — ZeaVis Edu
Layanan inferensi machine learning berbasis Rust dengan Axum web framework dan ONNX Runtime untuk klasifikasi penyakit daun jagung. Service ini menyediakan endpoint HTTP untuk prediksi real-time dengan performa tinggi dan konsumsi resource minimal.
> Layanan inferensi machine learning berbasis Rust/Axum + ONNX Runtime untuk klasifikasi penyakit daun jagung.
## Fitur
← [Kembali ke README utama](../../README.md)
---
## Daftar Isi
1. [Fitur](#1-fitur)
2. [Prasyarat & Instalasi](#2-prasyarat--instalasi)
3. [Menjalankan Service](#3-menjalankan-service)
4. [Environment Variables](#4-environment-variables)
5. [Endpoint API](#5-endpoint-api)
6. [Verifikasi & Testing](#6-verifikasi--testing)
7. [Docker Deployment](#7-docker-deployment)
8. [Troubleshooting](#8-troubleshooting)
---
## 1. Fitur
- **Framework:** Axum (async Rust web framework)
- **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) &bull; [Pipeline ML →](../../Machine_Learning/README.md) &bull; [Infra →](../../infra/README.md)
+2
View File
@@ -12,5 +12,7 @@ tauri-build = { version = "2", features = [] }
[dependencies]
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"
+4 -1
View File
@@ -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"
]
}
+3
View File
@@ -12,5 +12,8 @@
},
"devDependencies": {
"@tauri-apps/cli": "^2"
},
"dependencies": {
"sharp": "0.35.1"
}
}
+65
View File
@@ -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);
});
+32 -7
View File
@@ -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."
+2
View File
@@ -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");
}
+4 -4
View File
@@ -4,13 +4,13 @@
"version": "0.1.0",
"identifier": "com.zeavis.edu",
"build": {
"frontendDist": "../web/dist",
"frontendDist": "../web/dist-tauri",
"devUrl": "http://localhost:5173",
"beforeBuildCommand": "cd ../web && bun run build",
"beforeDevCommand": "cd ../web && bun run dev"
"beforeBuildCommand": "echo 'Nothing to build (APK loads live web content)'",
"beforeDevCommand": "echo 'Dev mode - connect to localhost:5173'"
},
"app": {
"withGlobalTauri": false,
"withGlobalTauri": true,
"windows": [
{
"title": "ZeaVis Edu",
+44
View File
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ZeaVis Edu</title>
<script>
var LIVE = 'https://zeavisedu.asepharyana.my.id';
var T = window.__TAURI_INTERNALS__;
function navigate(path) {
window.location.replace(LIVE + path);
}
// On cold start, check if the app was opened via a deep link (Google OAuth)
// before redirecting to the live web app.
if (T && T.invoke) {
T.invoke('plugin:deep-link|get_current')
.then(function(urls) {
if (urls && urls.length > 0 && urls[0]) {
try {
var u = new URL(urls[0]);
var target = u.pathname + u.search + u.hash;
if (target && target !== '/') {
// Preserve full path + query (e.g. /login?token=xxx)
navigate(target);
return;
}
} catch (e) { /* malformed URL — fall through */ }
}
// No deep link — redirect to live app home
navigate('/');
})
.catch(function() { navigate('/'); });
} else {
// Not in Tauri (dev mode or unknown) — redirect to live
navigate('/');
}
</script>
</head>
<body style="background:#f0fdf4;font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0">
<p style="color:#16a34a">Memuat ZeaVis Edu...</p>
</body>
</html>
+2
View File
@@ -12,6 +12,8 @@
"dependencies": {
"@radix-ui/react-slot": "^1.2.4",
"@tanstack/react-query": "^5.100.11",
"@tauri-apps/plugin-deep-link": "^2",
"@tauri-apps/plugin-opener": "^2",
"@vitejs/plugin-react": "^6.0.2",
"@zeavis/shared": "workspace:*",
"class-variance-authority": "^0.7.1",
+6
View File
@@ -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

+26 -8
View File
@@ -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>
+70
View File
@@ -0,0 +1,70 @@
/**
* Lightweight Tauri environment detection and utilities.
* Avoids importing @tauri-apps/api at module level so the web build
* doesn't bundle Tauri internals.
*/
let _isTauri: boolean | null = null;
export function isTauri(): boolean {
if (_isTauri !== null) return _isTauri;
_isTauri =
typeof window !== 'undefined' &&
'__TAURI_INTERNALS__' in window;
return _isTauri;
}
export async function openUrl(url: string): Promise<void> {
if (!isTauri()) {
window.location.href = url;
return;
}
const { openUrl: tauriOpenUrl } = await import('@tauri-apps/plugin-opener');
await tauriOpenUrl(url);
}
/**
* Listen for deep link URLs when the app is opened from an external link.
* On Android, after Google OAuth completes in the system browser, the
* callback page redirects to zeavisedu://... which triggers this listener.
* We extract the path + query and navigate the WebView there.
*/
function processDeepLinkUrl(url: string): void {
// url looks like: zeavisedu://login/login?token=xxx
// We need to extract path+query and navigate the WebView there
try {
const u = new URL(url);
const target = u.pathname + u.search + u.hash;
if (target && target !== '/') {
window.location.href = target;
return;
}
} catch { /* try fallback below */ }
// Fallback: handle both double-slash (://) and single-slash (:/) 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;
const { onOpenUrl, getCurrent } = await import('@tauri-apps/plugin-deep-link');
// Handle cold-start deep links (app opened via intent)
getCurrent().then((urls) => {
if (urls?.[0]) processDeepLinkUrl(urls[0]);
}).catch(() => { /* ignore */ });
// Handle warm-start deep links (app already running, new intent received)
onOpenUrl((urls) => {
for (const url of urls) {
processDeepLinkUrl(url);
}
});
}
+3 -3
View File
@@ -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>
+43 -29
View File
@@ -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.
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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
+70 -7
View File
@@ -1,15 +1,48 @@
import { useState } from "react";
import { useState, useEffect, useRef } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AuthForm } from "@/components/auth-form";
import { apiClient } from "@/lib/api-client";
import { apiClient, setAuthToken } from "@/lib/api-client";
import { useAuthStore } from "@/store/auth-store";
function getUrlParam(name: string): string | null {
return new URLSearchParams(window.location.search).get(name);
}
export function LoginPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const setUser = useAuthStore((state) => state.setUser);
const [error, setError] = useState<string | null>(null);
const oauthTokenConsumed = useRef(false);
const [oauthProcessing, setOauthProcessing] = useState(false);
// Handle OAuth callback: the API redirects to /login?token=<session_token>
useEffect(() => {
const token = getUrlParam("token");
if (!token || oauthTokenConsumed.current) return;
oauthTokenConsumed.current = true;
setOauthProcessing(true);
// Store token for future API calls and fetch user
setAuthToken(token);
apiClient
.getMe()
.then((data) => {
setUser(data.user);
queryClient.setQueryData(["auth", "me"], data);
navigate("/dashboard", { replace: true });
})
.catch((err) => {
setAuthToken(null);
setOauthProcessing(false);
setError(err instanceof Error ? err.message : "Google login gagal");
});
}, [setUser, queryClient, navigate]);
// Show OAuth error from query param
const oauthError = getUrlParam("error");
const meQuery = useQuery({
queryKey: ["auth", "me"],
queryFn: () => apiClient.getMe(),
@@ -26,13 +59,39 @@ export function LoginPage() {
setError(err instanceof Error ? err.message : "Login gagal"),
});
// Show loading spinner while OAuth token is being processed
if (oauthProcessing) {
return (
<main className="flex min-h-screen items-center justify-center px-6 py-12">
<div className="flex flex-col items-center gap-3">
<div className="h-10 w-10 border-4 border-green-500 border-t-transparent rounded-full animate-spin" />
<p className="text-gray-500 text-sm">
Menyelesaikan login dengan Google...
</p>
</div>
</main>
);
}
return (
<main className="flex min-h-screen items-center justify-center px-6 py-12">
<div className="w-full max-w-sm md:max-w-md space-y-4">
<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}
error={error}
error={oauthError || error}
googleOAuthEnabled={Boolean(
meQuery.data?.features.googleOAuthEnabled,
)}
@@ -42,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>
+12 -1
View File
@@ -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}
+19 -18
View File
@@ -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">
+3 -4
View File
@@ -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
View File
@@ -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,
+76 -1
View File
@@ -4,6 +4,10 @@
"workspaces": {
"": {
"name": "zeavis-edu",
"dependencies": {
"@tauri-apps/plugin-deep-link": "2.4.9",
"@tauri-apps/plugin-opener": "2.5.4",
},
"devDependencies": {
"@moonrepo/cli": "^2.2.5",
"typescript": "^6.0.3",
@@ -36,6 +40,9 @@
"apps/tauri": {
"name": "@zeavis/tauri",
"version": "0.1.0",
"dependencies": {
"sharp": "0.35.1",
},
"devDependencies": {
"@tauri-apps/cli": "^2",
},
@@ -46,6 +53,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 +98,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 +162,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 +404,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 +430,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 +674,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 +744,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
View File
@@ -1,6 +1,25 @@
# Infra — ZeaVis Edu Multi-VPS Deployment
# Infrastruktur — ZeaVis Edu
## Arsitektur
> Arsitektur multi-VPS untuk deployment produksi ZeaVis Edu dengan Tailscale mesh VPN dan observabilitas penuh.
← [Kembali ke README utama](../README.md)
---
## Daftar Isi
1. [Arsitektur](#1-arsitektur)
2. [Prasyarat GitHub Secrets](#2-prasyarat-github-secrets)
3. [Setup VPS](#3-setup-vps)
4. [Port yang Dibuka](#4-port-yang-dibuka)
5. [Metrics Flow](#5-metrics-flow)
6. [Perintah Penting](#6-perintah-penting)
---
## 1. Arsitektur
ZeaVis Edu berjalan di **dua VPS terpisah** yang terhubung melalui **Tailscale** mesh VPN:
```
┌─────────────────────────────────────────────┐ ┌──────────────────────────────────────────────┐
@@ -44,32 +63,41 @@
└─────────────────────────────────────────────┘ └──────────────────────────────────────────────┘
```
## Prerequisites
| VPS | Hostname | OS | Peran |
|---|---|---|---|
| **App VPS** | `imrnes` | Arch Linux | Web (:80), API (:3000), ML Service (:8000) |
| **Telemetry VPS** | `orange` | Ubuntu | Prometheus, ClickHouse, Telemetry UI |
### GitHub Secrets (untuk CI/CD)
---
**App VPS deploy (`.github/workflows/deploy.yml`):**
| 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) &bull; [ML Service →](../apps/ml-service/README.md) &bull; [Pipeline ML →](../Machine_Learning/README.md)
+5 -1
View File
@@ -13,5 +13,9 @@
"workspaces": [
"apps/*",
"packages/*"
]
],
"dependencies": {
"@tauri-apps/plugin-deep-link": "2.4.9",
"@tauri-apps/plugin-opener": "2.5.4"
}
}