Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4bd114eed | ||
|
|
7423e4d83b | ||
|
|
4b252a1d5d | ||
|
|
7148e1026e | ||
|
|
96f337ef46 | ||
|
|
3b5839ebd5 | ||
|
|
e2742a7a60 | ||
|
|
ae22adee40 | ||
|
|
ee02bd4f9a | ||
|
|
310d8adc8d | ||
|
|
ccbcd99053 | ||
|
|
52e60b99dd | ||
|
|
e19321a2b7 | ||
|
|
d1275b5b65 | ||
|
|
12162fd6f7 | ||
|
|
9ab8d41adf | ||
|
|
6f07fc05ac | ||
|
|
cbcc7da33d | ||
|
|
d1282364e7 | ||
|
|
254fe19bb6 | ||
|
|
99acc43ff0 | ||
|
|
1afd2e013c | ||
|
|
e1732f5111 | ||
|
|
fc832b404e | ||
|
|
6ba3deb149 | ||
|
|
ac59548337 | ||
|
|
d292bc4b3c | ||
|
|
787acf077f | ||
|
|
250355562f | ||
|
|
d47c73308e | ||
|
|
4b1d70d1c4 | ||
|
|
c9ae90a042 | ||
|
|
caca13e32c | ||
|
|
a55b1521ea | ||
|
|
2eabf12b36 | ||
|
|
b18b98e1a7 | ||
|
|
a36b74a1cf | ||
|
|
9cfbef4598 | ||
|
|
8f1308c8af | ||
|
|
c881e6a2b2 | ||
|
|
7693a02c98 | ||
|
|
528a1622d0 | ||
|
|
51a4cb9ed2 | ||
|
|
22307d44de | ||
|
|
41e0501e6b | ||
|
|
f675b86dc8 | ||
|
|
8ed1b57f1d | ||
|
|
bf1725a4b8 | ||
|
|
8954535e08 | ||
|
|
b93be6c6cb | ||
|
|
2d39468279 | ||
|
|
78db3a03ac | ||
|
|
bed0e018db | ||
|
|
3b37a92d1e | ||
|
|
6c1006d4a0 | ||
|
|
03c1e4edb8 | ||
|
|
56b683392c | ||
|
|
05055ed9cf | ||
|
|
4b5530ef73 | ||
|
|
02ae022174 | ||
|
|
61a4043967 | ||
|
|
9ee3af8a1f | ||
|
|
3d3ca72230 |
@@ -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 |
@@ -18,7 +18,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL || 'https://zeavisedu.asepharyana.tech' }}
|
VITE_API_BASE_URL: ${{ vars.VITE_API_BASE_URL || 'https://zeavisedu.asepharyana.my.id' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-apk:
|
build-apk:
|
||||||
@@ -40,6 +40,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Cache Bun dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.bun/install/cache
|
||||||
|
node_modules
|
||||||
|
apps/*/node_modules
|
||||||
|
packages/*/node_modules
|
||||||
|
key: ${{ runner.os }}-bun-${{ hashFiles('bun.lock', '**/package.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-bun-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install
|
run: bun install
|
||||||
|
|
||||||
@@ -52,7 +64,9 @@ jobs:
|
|||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@v3
|
uses: android-actions/setup-android@v3
|
||||||
with:
|
with:
|
||||||
packages: 'platforms;android-36 build-tools;36.0.0 ndk;28.0.13004108'
|
packages: 'platforms;android-36 build-tools;36.0.0'
|
||||||
|
- name: Pin NDK version
|
||||||
|
run: echo "ANDROID_NDK_HOME=${ANDROID_SDK_ROOT}/ndk/$(ls ${ANDROID_SDK_ROOT}/ndk | sort -V | head -1)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Setup Rust with Android targets
|
- name: Setup Rust with Android targets
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
@@ -74,6 +88,16 @@ jobs:
|
|||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-cargo-android-
|
${{ runner.os }}-cargo-android-
|
||||||
|
|
||||||
|
- name: Cache Gradle
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.gradle/caches
|
||||||
|
~/.gradle/wrapper
|
||||||
|
key: ${{ runner.os }}-gradle-android-${{ hashFiles('apps/tauri/Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gradle-android-
|
||||||
|
|
||||||
- name: Install Tauri CLI
|
- name: Install Tauri CLI
|
||||||
run: cd apps/tauri && bun install
|
run: cd apps/tauri && bun install
|
||||||
|
|
||||||
@@ -108,7 +132,17 @@ jobs:
|
|||||||
JAVA_HOME: ${{ env.JAVA_HOME_21_X64 }}
|
JAVA_HOME: ${{ env.JAVA_HOME_21_X64 }}
|
||||||
ANDROID_HOME: ${{ env.ANDROID_SDK_ROOT }}
|
ANDROID_HOME: ${{ env.ANDROID_SDK_ROOT }}
|
||||||
NDK_HOME: ${{ env.ANDROID_NDK_HOME }}
|
NDK_HOME: ${{ env.ANDROID_NDK_HOME }}
|
||||||
run: bun tauri android init
|
run: |
|
||||||
|
rm -rf gen/android
|
||||||
|
bun tauri android init
|
||||||
|
|
||||||
|
- 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
|
||||||
|
|
||||||
- name: Build Tauri Android APK
|
- name: Build Tauri Android APK
|
||||||
working-directory: apps/tauri
|
working-directory: apps/tauri
|
||||||
@@ -116,7 +150,7 @@ jobs:
|
|||||||
JAVA_HOME: ${{ env.JAVA_HOME_21_X64 }}
|
JAVA_HOME: ${{ env.JAVA_HOME_21_X64 }}
|
||||||
ANDROID_HOME: ${{ env.ANDROID_SDK_ROOT }}
|
ANDROID_HOME: ${{ env.ANDROID_SDK_ROOT }}
|
||||||
NDK_HOME: ${{ env.ANDROID_NDK_HOME }}
|
NDK_HOME: ${{ env.ANDROID_NDK_HOME }}
|
||||||
run: bun tauri android build
|
run: bun tauri android build --apk
|
||||||
|
|
||||||
- name: Decode keystore
|
- name: Decode keystore
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
|
|||||||
@@ -177,8 +177,11 @@ jobs:
|
|||||||
cat << 'ENVEOF'
|
cat << 'ENVEOF'
|
||||||
DATABASE_URL=${{ secrets.DATABASE_URL }}
|
DATABASE_URL=${{ secrets.DATABASE_URL }}
|
||||||
SESSION_SECRET=${{ secrets.SESSION_SECRET }}
|
SESSION_SECRET=${{ secrets.SESSION_SECRET }}
|
||||||
WEB_APP_URL=https://zeavisedu.asepharyana.tech
|
WEB_APP_URL=https://zeavisedu.asepharyana.my.id
|
||||||
ML_SERVICE_URL=http://zeavis-ml:8000
|
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
|
ENVEOF
|
||||||
} > .env
|
} > .env
|
||||||
|
|
||||||
@@ -189,8 +192,17 @@ jobs:
|
|||||||
docker rm -f zeavis-web zeavis-api zeavis-ml zeavis-node-exporter 2>/dev/null || true
|
docker rm -f zeavis-web zeavis-api zeavis-ml zeavis-node-exporter 2>/dev/null || true
|
||||||
docker compose pull
|
docker compose pull
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
# Wait for containers to be healthy (up to 60s)
|
||||||
|
wait_container() {
|
||||||
|
local name=$1
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
docker compose ps | grep -q "${name}.*Up" && return 0
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
wait_container zeavis-web || exit 1
|
||||||
|
wait_container zeavis-api || exit 1
|
||||||
|
wait_container zeavis-ml || exit 1
|
||||||
|
wait_container zeavis-node-exporter || echo "⚠️ node_exporter not running (non-fatal)"
|
||||||
docker compose ps
|
docker compose ps
|
||||||
docker compose ps | grep -q "zeavis-web.*Up" || exit 1
|
|
||||||
docker compose ps | grep -q "zeavis-api.*Up" || exit 1
|
|
||||||
docker compose ps | grep -q "zeavis-ml.*Up" || exit 1
|
|
||||||
docker compose ps | grep -q "zeavis-node-exporter.*Up" || echo "⚠️ node_exporter not running (non-fatal)"
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
# Corn Leaf Disease Classification
|
# Pipeline Machine Learning — ZeaVis Edu
|
||||||
|
|
||||||
Pipeline lengkap untuk klasifikasi penyakit daun jagung menggunakan **EfficientNetV2B0**, mulai dari persiapan dataset, pelatihan di Google Colab, hingga ekspor model ke format **TFLite** dan **TensorFlow.js** untuk kebutuhan produksi.
|
> Panduan lengkap: preprocessing dataset, pelatihan di Google Colab, ekspor model ke TFLite, TensorFlow.js, dan ONNX.
|
||||||
|
|
||||||
|
← [Kembali ke README utama](../README.md)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -390,4 +392,8 @@ export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
|
|||||||
---
|
---
|
||||||
|
|
||||||
### Sesi Colab terputus saat training
|
### Sesi Colab terputus saat training
|
||||||
**Solusi:** Gunakan callback `ModelCheckpoint` di notebook untuk menyimpan checkpoint secara berkala ke Google Drive, sehingga training bisa dilanjutkan dari checkpoint terakhir tanpa mengulang dari awal.
|
**Solusi:** Gunakan callback `ModelCheckpoint` di notebook untuk menyimpan checkpoint secara berkala ke Google Drive, sehingga training bisa dilanjutkan dari checkpoint terakhir tanpa mengulang dari awal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
← [Kembali ke README utama](../README.md) • [ML Service →](../apps/ml-service/README.md)
|
||||||
|
|||||||
@@ -1,482 +1,326 @@
|
|||||||
# ZeaVis Edu
|
<p align="center">
|
||||||
|
<br>
|
||||||
|
<img src=".github/assets/zeavis-logo.svg" alt="ZeaVis Edu" width="128"><br>
|
||||||
|
<h1 align="center">ZeaVis Edu</h1>
|
||||||
|
<p align="center">
|
||||||
|
<strong>Asisten Edukasi Interaktif untuk Deteksi Penyakit Daun Jagung</strong><br>
|
||||||
|
<em>Computer Vision • EfficientNetV2B0 • Rust ONNX Runtime • Tauri 2 Android</em>
|
||||||
|
</p>
|
||||||
|
</p>
|
||||||
|
|
||||||
ZeaVis Edu adalah aplikasi edukasi untuk membantu mengenali penyakit daun jagung melalui klasifikasi gambar berbasis machine learning. Repositori ini menggabungkan aplikasi web, API backend, layanan inferensi ML, serta pipeline pelatihan dan ekspor model EfficientNetV2B0.
|
<p align="center">
|
||||||
|
<a href="#-tentang"><b>Tentang</b></a> •
|
||||||
|
<a href="#-tim"><b>Tim</b></a> •
|
||||||
|
<a href="#-ringkasan-eksekutif"><b>Ringkasan</b></a> •
|
||||||
|
<a href="#-cakupan--deliverables"><b>Cakupan</b></a> •
|
||||||
|
<a href="#-jadwal"><b>Jadwal</b></a> •
|
||||||
|
<a href="#-tech-stack"><b>Tech Stack</b></a> •
|
||||||
|
<a href="#-memulai"><b>Memulai</b></a> •
|
||||||
|
<a href="#-platform"><b>Platform</b></a> •
|
||||||
|
<a href="#-dokumentasi"><b>Dokumentasi</b></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
## Fitur Utama
|
<br>
|
||||||
|
|
||||||
- Aplikasi web untuk pengalaman pengguna dan interaksi edukatif.
|
---
|
||||||
- API backend untuk status layanan, integrasi data, dan komunikasi dengan layanan ML.
|
|
||||||
- ML service berbasis Rust/Axum dengan ONNX Runtime untuk inferensi penyakit daun jagung dari gambar.
|
|
||||||
- Pipeline machine learning untuk preprocessing dataset, training di Google Colab, dan ekspor model produksi.
|
|
||||||
- Dukungan Docker untuk deployment web, API, dan ML service.
|
|
||||||
- Workspace monorepo berbasis Bun dan Moon untuk menjalankan task development, typecheck, dan build secara terpusat.
|
|
||||||
|
|
||||||
## Kelas Penyakit
|
## 🌽 Tentang
|
||||||
|
|
||||||
Model klasifikasi menargetkan empat label berbahasa Indonesia:
|
**ZeaVis Edu** adalah aplikasi edukasi berbasis **Computer Vision** yang membantu petani, mahasiswa pertanian, dan penyuluh lapangan mengidentifikasi penyakit daun jagung secara mandiri — cukup dengan mengunggah foto daun jagung.
|
||||||
|
|
||||||
| Label | Deskripsi |
|
Proyek ini merupakan **Capstone Project** dalam program **Pijak × IBM SkillsBuild** dengan tema **"AI for Smart Education"**, dirancang untuk menjembatani kesenjangan antara pengetahuan teori pertanian dan kebutuhan praktis di lapangan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👥 Tim
|
||||||
|
|
||||||
|
| NPM | Nama | Learning Path | Peran |
|
||||||
|
|---|---|---|---|
|
||||||
|
| APC246D6Y0028 | **Asep Haryana Saputra** | Back-End | Arsitektur sistem, RESTful API, deployment Docker/Cloud, keamanan upload stream |
|
||||||
|
| APC013D6X0081 | **Selly Supriyatin** | Front-End | UI/UX responsif, mekanisme unggah gambar, modul edukasi (rekomendasi obat & penanganan) |
|
||||||
|
| APC013D6Y0091 | **Taufik Pathurrohman** | Machine Learning | Data Engineering — ekstraksi dataset, cleaning, augmentasi gambar |
|
||||||
|
| APC414D6Y0138 | **Luhung Pandyaska Suyi** | Machine Learning | Model Architecture & Training — CNN, hyperparameter tuning |
|
||||||
|
| APC013D6Y0269 | **Ardian** | Machine Learning | Model Evaluation & Deployment Prep — confusion matrix, konversi ke production-ready |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Ringkasan Eksekutif
|
||||||
|
|
||||||
|
### Masalah
|
||||||
|
|
||||||
|
Data BPS menunjukkan penurunan luas panen jagung dari **2.764.366 Ha (2022)** menjadi **2.487.191 Ha (2023)**. Salah satu penyebab utamanya adalah penyakit daun seperti **Hawar Daun**, **Karat Daun**, dan **Bercak Daun Abu-abu** yang menyebabkan nekrosis dan menghambat fotosintesis.
|
||||||
|
|
||||||
|
Petani sering kesulitan mengidentifikasi penyakit secara kasat mata dan memiliki **ketergantungan tinggi pada POPT** (Petugas Pengendali Organisme Pengganggu Tumbuhan) akibat minimnya media pembelajaran interaktif.
|
||||||
|
|
||||||
|
### Solusi
|
||||||
|
|
||||||
|
ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
|
||||||
|
|
||||||
|
1. 📸 **Unggah** foto daun jagung yang diduga terinfeksi
|
||||||
|
2. 🤖 **Deteksi otomatis** penyakit oleh model AI (EfficientNetV2B0)
|
||||||
|
3. 📚 **Dapatkan** informasi detail penyakit, panduan pencegahan, dan rekomendasi obat secara mandiri
|
||||||
|
|
||||||
|
### Metode Teknis
|
||||||
|
|
||||||
|
| Komponen | Pilihan |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Bercak Daun | Gray Leaf Spot |
|
| Arsitektur Model | **EfficientNetV2B0** — keseimbangan optimal antara akurasi dan efisiensi parameter |
|
||||||
| Hawar Daun | Northern/Southern Leaf Blight |
|
| Metode Pelatihan | **Transfer Learning** pada Google Colab (GPU T4) |
|
||||||
| Karat Daun | Common Rust |
|
| Sumber Dataset | Kaggle — [Corn Leaf Disease](https://www.kaggle.com/datasets/ndisan/corn-leaf-disease) |
|
||||||
| Daun Sehat | Daun jagung tanpa gejala penyakit |
|
| Deployment | VPS dengan Docker, ONNX Runtime untuk inferensi real-time |
|
||||||
|
|
||||||
## Struktur Proyek
|
---
|
||||||
|
|
||||||
```text
|
## 🎯 Cakupan & Deliverables
|
||||||
|
|
||||||
|
### Cakupan
|
||||||
|
|
||||||
|
| ✅ Dalam Cakupan | ❌ Di Luar Cakupan |
|
||||||
|
|---|---|
|
||||||
|
| Klasifikasi 3 penyakit + 1 daun sehat | Penyakit pada batang atau buah jagung |
|
||||||
|
| Deteksi berbasis unggah gambar daun | Prediksi tanpa input gambar |
|
||||||
|
| Rekomendasi obat & penanganan | Diagnosis pengganti ahli/POPT |
|
||||||
|
| Aplikasi Web + Android (Tauri 2) | Aplikasi iOS |
|
||||||
|
|
||||||
|
### 4 Kelas yang Diklasifikasikan
|
||||||
|
|
||||||
|
| Label | Nama Ilmiah | Gejala |
|
||||||
|
|---|---|---|
|
||||||
|
| **Hawar Daun** | *Northern/Southern Leaf Blight* | Hawar coklat memanjang pada daun |
|
||||||
|
| **Karat Daun** | *Common Rust* | Bintik coklat kemerahan berbentuk pustula |
|
||||||
|
| **Bercak Daun** | *Gray Leaf Spot* | Bercak abu-abu memanjang |
|
||||||
|
| **Daun Sehat** | — | Tanpa gejala penyakit |
|
||||||
|
|
||||||
|
### Deliverables Proyek
|
||||||
|
|
||||||
|
| No | Tahapan | Deskripsi |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | **Pengumpulan Data** | Dataset gambar 3 penyakit + 1 daun sehat dari Kaggle beserta pelabelan |
|
||||||
|
| 2 | **Model ML** | Model Computer Vision terlatih di Google Colab, siap produksi |
|
||||||
|
| 3 | **UI Antarmuka** | Front-End berbasis React + Vite dengan fitur unggah gambar |
|
||||||
|
| 4 | **Back-End Integration** | API + ML Service untuk inferensi real-time via Docker |
|
||||||
|
| 5 | **Prototipe Akhir** | Aplikasi Web + Android (Tauri 2) dengan klasifikasi & modul edukasi (rekomendasi obat & penanganan) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📅 Jadwal
|
||||||
|
|
||||||
|
| Minggu | Tanggal | Fase | Aktivitas |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **1** | 11–17 Mei 2026 | Inisiasi & Data | Spesifikasi teknis (Asep) • Dataset dari Kaggle + preprocessing (Taufik) • Wireframe UI/UX (Selly) |
|
||||||
|
| **2** | 18–24 Mei 2026 | Training & Dev Awal | Implementasi EfficientNetV2B0 di Colab (Luhung) • Slicing UI ke React (Selly) • Setup server, database, routing API (Asep) |
|
||||||
|
| **3** | 25–31 Mei 2026 | Evaluasi & Modul Edukasi | Evaluasi akurasi + konversi model ke ONNX/TFLite (Ardian) • Halaman edukasi obat & penanganan (Selly) • RESTful API untuk image upload & inferensi (Asep) |
|
||||||
|
| **4** | 1–7 Juni 2026 | Integrasi & Testing | Integrasi penuh Front-End ↔ API ↔ Model ML • Pengujian end-to-end • Stress testing & error handling (Semua) |
|
||||||
|
| **5** | 8–14 Juni 2026 | Deployment & Finalisasi | Deployment ke VPS (Asep) • Bug fixing & optimalisasi UI/UX (Selly) • Dokumentasi teknis & materi presentasi (Semua) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Manajemen Risiko
|
||||||
|
|
||||||
|
| Risiko | Solusi |
|
||||||
|
|---|---|
|
||||||
|
| **Overfitting akibat imbalanced data** | Augmentasi tingkat lanjut (kecerahan, noise, rotasi) + confidence threshold < 75% → minta user foto ulang |
|
||||||
|
| **Server downtime / latensi tinggi** | Batasan upload ≤ 5 MB + kompresi server-side + rate limiting + container Docker isolasi resource |
|
||||||
|
| **Foto blur / objek bukan daun jagung** | Panduan visual (overlay) pada UI + validasi anomali + disclaimer "alat bantu edukasi, bukan pengganti POPT" |
|
||||||
|
| **Bottleneck integrasi ML ↔ API ↔ UI** | API Contract ketat di minggu ke-1 + integrasi bertahap (CI) mulai minggu ke-3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Arsitektur Proyek
|
||||||
|
|
||||||
|
```
|
||||||
.
|
.
|
||||||
├── apps/
|
├── apps/
|
||||||
│ ├── api/ # Backend Elysia/Bun
|
│ ├── api/ # Backend Elysia/Bun + Drizzle ORM + PostgreSQL
|
||||||
│ ├── ml-service/ # Layanan inferensi Rust/Axum + ONNX Runtime
|
│ ├── ml-service/ # Rust/Axum + ONNX Runtime inference engine
|
||||||
│ └── web/ # Frontend React + Vite
|
│ ├── tauri/ # Tauri 2 mobile wrapper → Android APK
|
||||||
├── Machine_Learning/ # Pipeline dataset, training, dan ekspor model
|
│ └── web/ # Frontend React + Vite + Tailwind CSS
|
||||||
├── packages/
|
├── Machine_Learning/ # Pipeline dataset, training Colab, ekspor model
|
||||||
│ └── shared/ # Tipe dan utilitas bersama TypeScript
|
│ └── README.md # ⤷ Panduan lengkap pipeline ML
|
||||||
├── docker-compose.yml # Konfigurasi deployment container
|
├── infra/
|
||||||
├── package.json # Script dan workspace root Bun
|
│ └── README.md # ⤷ Panduan deployment multi-VPS
|
||||||
└── README.md # Dokumentasi utama proyek
|
├── 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
|
## 🛠️ Tech Stack
|
||||||
- Vite
|
|
||||||
- TypeScript
|
### Frontend & Mobile
|
||||||
- React Router
|
React • Vite • TypeScript • React Router • TanStack Query • Zustand • Tailwind CSS
|
||||||
- TanStack Query
|
**Tauri 2** (Android) • Rust • WebView • Deep Link OAuth
|
||||||
- Zustand
|
|
||||||
- Tailwind CSS
|
|
||||||
|
|
||||||
### Backend API
|
### Backend API
|
||||||
|
Bun • Elysia • Drizzle ORM • PostgreSQL • prom-client
|
||||||
- Bun
|
|
||||||
- Elysia
|
|
||||||
- Drizzle ORM
|
|
||||||
- PostgreSQL
|
|
||||||
|
|
||||||
### Machine Learning
|
### Machine Learning
|
||||||
|
Python • TensorFlow/Keras • EfficientNetV2B0 • Google Colab (GPU T4)
|
||||||
|
|
||||||
- Python (preprocessing, training, export)
|
### Inference Engine
|
||||||
- TensorFlow/Keras
|
**Rust** • **Axum** • **ONNX Runtime** • TFLite • TensorFlow.js
|
||||||
- EfficientNetV2B0
|
|
||||||
- Rust
|
|
||||||
- Axum
|
|
||||||
- ONNX Runtime
|
|
||||||
- TFLite
|
|
||||||
- TensorFlow.js
|
|
||||||
|
|
||||||
### Tooling & Deployment
|
### DevOps & Infrastruktur
|
||||||
|
Docker • Docker Compose • Coolify • Traefik • Tailscale • GitHub Actions (CI/CD)
|
||||||
|
|
||||||
- Bun workspaces
|
### Observabilitas
|
||||||
- Moon task runner
|
Prometheus • Metric Ingester (Go) • Vector • ClickHouse • Query Proxy (Go) • Telemetry UI (Vue 3)
|
||||||
- Docker
|
|
||||||
- Docker Compose
|
|
||||||
- GitHub Container Registry
|
|
||||||
- Traefik labels untuk routing deployment
|
|
||||||
|
|
||||||
### Telemetry & Observability
|
---
|
||||||
|
|
||||||
- Prometheus — metric scraping & remote_write
|
## 🚀 Memulai
|
||||||
- Metric Ingester (Go) — enrichment, filtering, aggregation
|
|
||||||
- Vector — buffering & backpressure
|
|
||||||
- ClickHouse — columnar analytical storage
|
|
||||||
- Query Proxy (Go) — read-only SQL proxy
|
|
||||||
- Telemetry UI (Vue 3) — metrics dashboard
|
|
||||||
- Semua service ZeaVis Edu (web, api, ml-service) mengekspos metrik Prometheus di `/metrics`
|
|
||||||
- Client-side Web Vitals (CLS, FCP, INP, LCP, TTFB) dikumpulkan di frontend
|
|
||||||
|
|
||||||
## Prasyarat
|
### Prasyarat
|
||||||
|
|
||||||
Untuk menjalankan seluruh project secara lokal, siapkan:
|
- **Bun** — runtime & package manager
|
||||||
|
- **Python 3.9–3.11** — pipeline ML
|
||||||
|
- **Rust & Cargo** — `apps/ml-service` (inference) & `apps/tauri` (Android)
|
||||||
|
- **Java 21 + Android SDK** — build Android APK
|
||||||
|
- **Docker & Docker Compose** — deployment & telemetry
|
||||||
|
- **PostgreSQL** — backend API
|
||||||
|
|
||||||
- Bun
|
### Instalasi
|
||||||
- Python 3.9–3.11 untuk pipeline ML
|
|
||||||
- Rust dan Cargo untuk `apps/ml-service`
|
|
||||||
- Docker dan Docker Compose jika ingin menjalankan/deploy via container
|
|
||||||
- PostgreSQL jika fitur backend yang membutuhkan database digunakan
|
|
||||||
- File model `Machine_Learning/model/model.onnx` untuk inferensi ML lokal
|
|
||||||
|
|
||||||
## Instalasi Root Workspace
|
|
||||||
|
|
||||||
Jalankan dari root repository:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
git clone https://github.com/ATLAS-PJK-GM007/ZeaVis-Edu.git
|
||||||
|
cd ZeaVis-Edu
|
||||||
bun install
|
bun install
|
||||||
```
|
```
|
||||||
|
|
||||||
## Menjalankan Project Lokal
|
### Menjalankan Development
|
||||||
|
|
||||||
### Menjalankan Semua Task Development
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun run dev
|
bun run dev # Semua service (web + api)
|
||||||
|
cd apps/web && bun run dev # Hanya frontend
|
||||||
|
cd apps/api && bun run start # Hanya backend API
|
||||||
|
cd apps/ml-service && cargo run # 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
|
| Variable | Keterangan |
|
||||||
bun run typecheck
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build Produksi
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Menjalankan Service Secara Terpisah
|
|
||||||
|
|
||||||
### Web App
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/web
|
|
||||||
bun run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Secara default Vite akan menjalankan server development dan menampilkan URL lokal di terminal.
|
|
||||||
|
|
||||||
### API Backend
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/api
|
|
||||||
bun run start
|
|
||||||
```
|
|
||||||
|
|
||||||
API membaca konfigurasi dari file `.env` di root repository melalui script Bun.
|
|
||||||
|
|
||||||
Script lain yang tersedia:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
bun run db:generate
|
|
||||||
bun run db:migrate
|
|
||||||
bun run db:seed
|
|
||||||
bun run typecheck
|
|
||||||
```
|
|
||||||
|
|
||||||
### ML Service
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/ml-service
|
|
||||||
cargo run
|
|
||||||
```
|
|
||||||
|
|
||||||
Default path model adalah:
|
|
||||||
|
|
||||||
```text
|
|
||||||
../../Machine_Learning/model/model.onnx
|
|
||||||
```
|
|
||||||
|
|
||||||
Jika model berada di lokasi lain, gunakan environment variable `MODEL_PATH`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
MODEL_PATH=/path/to/model.onnx cargo run
|
|
||||||
```
|
|
||||||
|
|
||||||
**Port Configuration:**
|
|
||||||
|
|
||||||
- **Default (tanpa .env):** Service mendengarkan di `http://localhost:8000`
|
|
||||||
- **Local development (dengan .env.example):** Service mendengarkan di `http://localhost:8001`
|
|
||||||
```bash
|
|
||||||
cd apps/ml-service
|
|
||||||
source .env.example
|
|
||||||
cargo run
|
|
||||||
```
|
|
||||||
- **Docker container:** Service mendengarkan di port `8000`
|
|
||||||
|
|
||||||
Lihat `apps/ml-service/README.md` untuk detail lengkap tentang konfigurasi port dan contoh curl.
|
|
||||||
|
|
||||||
## Docker Deployment
|
|
||||||
|
|
||||||
File `docker-compose.yml` di root menyiapkan tiga service produksi:
|
|
||||||
|
|
||||||
- `web` untuk frontend
|
|
||||||
- `api` untuk backend
|
|
||||||
- `ml` untuk layanan inferensi machine learning
|
|
||||||
|
|
||||||
Konfigurasi compose menggunakan image dari GitHub Container Registry:
|
|
||||||
|
|
||||||
```text
|
|
||||||
ghcr.io/${GITHUB_REPOSITORY:-mytheclipse/zeavis-edu}/web:main
|
|
||||||
ghcr.io/${GITHUB_REPOSITORY:-mytheclipse/zeavis-edu}/api:main
|
|
||||||
ghcr.io/${GITHUB_REPOSITORY:-mytheclipse/zeavis-edu}/ml:main
|
|
||||||
```
|
|
||||||
|
|
||||||
Compose juga mengasumsikan network eksternal bernama `app-shared-net` dan routing Traefik untuk domain produksi. Service `ml` berjalan pada port `8000` di dalam container.
|
|
||||||
|
|
||||||
Contoh menjalankan compose setelah environment dan network siap:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Telemetry Stack
|
|
||||||
|
|
||||||
Proyek ini menyertakan pipeline telemetry metric sebagai git submodule di `telemetry/`. Pipeline mengalirkan metrik dari seluruh service ZeaVis Edu ke ClickHouse untuk analisis dan visualisasi jangka panjang.
|
|
||||||
|
|
||||||
### Arsitektur (Production)
|
|
||||||
|
|
||||||
Di production, aplikasi dan telemetry berjalan di **VPS terpisah** dan terhubung via **Tailscale** (mesh VPN). Prometheus di VPS telemetry melakukan scrape ke service ZeaVis Edu melalui IP Tailscale masing-masing.
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
flowchart LR
|
|
||||||
subgraph VPS1["VPS — ZeaVis Edu (App)"]
|
|
||||||
W[Web / React<br/>api-zeavisedu.asepharyana.id]
|
|
||||||
A[API / Elysia<br/>:3000]
|
|
||||||
M[ML Service / Axum<br/>:8000]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph VPS2["VPS — Telemetry Stack"]
|
|
||||||
P[Prometheus<br/>:9090]
|
|
||||||
MI[Metric Ingester<br/>:9091]
|
|
||||||
V[Vector<br/>:9001]
|
|
||||||
CH[ClickHouse<br/>:8123]
|
|
||||||
QP[Query Proxy<br/>:9092]
|
|
||||||
TUI[Telemetry UI<br/>:8181]
|
|
||||||
end
|
|
||||||
|
|
||||||
P -.->|"scrape via Tailscale IP<br/>100.x.x.a:3000/metrics"| A
|
|
||||||
P -.->|"scrape via Tailscale IP<br/>100.x.x.a:8000/metrics"| M
|
|
||||||
P -->|remote_write| MI
|
|
||||||
MI --> V
|
|
||||||
V --> CH
|
|
||||||
QP --> CH
|
|
||||||
TUI --> QP
|
|
||||||
```
|
|
||||||
|
|
||||||
Setiap service ZeaVis Edu mengekspos endpoint `/metrics` dalam format Prometheus text:
|
|
||||||
|
|
||||||
| Service | Endpoint | Port (lokal) |
|
|
||||||
|-----------------------|--------------------|--------------|
|
|
||||||
| Web (Vite dev) | `GET /metrics` | 5173 |
|
|
||||||
| API (Elysia) | `GET /metrics` | 3000 |
|
|
||||||
| ML Service (Axum) | `GET /metrics` | 8000 |
|
|
||||||
|
|
||||||
Prometheus di VPS telemetry melakukan **scrape langsung** ke API dan ML service melalui IP Tailscale mereka, bukan melalui domain publik. Konfigurasi target ada di `telemetry/prometheus/targets/zeavis-edu.json` — isi dengan IP Tailscale dari service yang dituju.
|
|
||||||
|
|
||||||
Lihat [`METRICS.md`](./METRICS.md) untuk daftar lengkap metrik yang diekspos.
|
|
||||||
|
|
||||||
### Service Telemetry
|
|
||||||
|
|
||||||
| # | Service | Peran | Port |
|
|
||||||
|---|---------|------|------|
|
|
||||||
| 1 | **Prometheus** | Metric scraping & remote_write | 9090 |
|
|
||||||
| 2 | **Metric Ingester** | Enrichment, filtering, aggregation | 9091 |
|
|
||||||
| 3 | **Vector** | Buffering, backpressure, retry | 9001 |
|
|
||||||
| 4 | **ClickHouse** | Columnar analytical storage | 8123 / 9000 |
|
|
||||||
| 5 | **Query Proxy** | Read-only SQL proxy, tenant isolation | 9092 |
|
|
||||||
| 6 | **Telemetry UI** | Vue 3 metrics dashboard | 8181 |
|
|
||||||
|
|
||||||
### Arsitektur (Local Dev)
|
|
||||||
|
|
||||||
Untuk development lokal di satu mesin, telemetry dan app bisa jalan bareng di satu Docker host. Prometheus bisa scrape service lewat Docker network yang sama.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Setup network
|
|
||||||
docker network create app-shared-net
|
|
||||||
|
|
||||||
# Build & start telemetry (dengan network sharing)
|
|
||||||
make telemetry-up-local
|
|
||||||
```
|
|
||||||
|
|
||||||
### Menjalankan Telemetry Stack
|
|
||||||
|
|
||||||
Semua operasi telemetry dijalankan dari **root proyek** melalui Makefile:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build komponen telemetry (metric-ingester + telemetry-ui)
|
|
||||||
make telemetry-build
|
|
||||||
|
|
||||||
# Start semua service telemetry (mode produksi, via Tailscale)
|
|
||||||
make telemetry-up
|
|
||||||
|
|
||||||
# Start semua service telemetry (mode lokal — port langsung terbuka)
|
|
||||||
make telemetry-up-local
|
|
||||||
|
|
||||||
# Cek status kesehatan semua service
|
|
||||||
make telemetry-status
|
|
||||||
|
|
||||||
# Lihat log (semua service, atau filter dengan s=)
|
|
||||||
make telemetry-logs
|
|
||||||
make telemetry-logs s=metric-ingester
|
|
||||||
|
|
||||||
# Restart service tertentu
|
|
||||||
make telemetry-restart s=prometheus
|
|
||||||
|
|
||||||
# Kirim test metric
|
|
||||||
make telemetry-test-metric
|
|
||||||
|
|
||||||
# Stop semua service
|
|
||||||
make telemetry-down
|
|
||||||
```
|
|
||||||
|
|
||||||
Untuk development lokal:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Setup network jika belum ada
|
|
||||||
docker network create telemetry-net
|
|
||||||
docker network create app-shared-net
|
|
||||||
|
|
||||||
# Build & start
|
|
||||||
make telemetry-build
|
|
||||||
make telemetry-up-local
|
|
||||||
|
|
||||||
# Buka dashboard di http://localhost:8181
|
|
||||||
```
|
|
||||||
|
|
||||||
### Prometheus Auto-Discovery
|
|
||||||
|
|
||||||
Prometheus menggunakan `file_sd_configs` untuk menemukan target secara dinamis. Cukup letakkan file JSON di `telemetry/prometheus/targets/` dan Prometheus akan otomatis mendeteksinya dalam 15 detik — tanpa restart.
|
|
||||||
|
|
||||||
File template sudah tersedia di [`telemetry/prometheus/targets/zeavis-edu.json`](telemetry/prometheus/targets/zeavis-edu.json). **Sebelum production, isi `__CHANGE_ME__` dengan IP Tailscale masing-masing service:**
|
|
||||||
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{ "targets": ["100.x.x.a:3000"], "labels": { "service": "zeavis-api", "component": "backend", "env": "production" } },
|
|
||||||
{ "targets": ["100.x.x.a:8000"], "labels": { "service": "zeavis-ml", "component": "inference", "env": "production" } }
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Catatan:** Aplikasi ZeaVis Edu mengekspose port Docker-nya (`:3000`, `:8000`) langsung ke host via `docker-compose.yml`. Pastikan port-port tersebut terbuka di network Tailscale (biasanya iptables Tailscale mengizinkan koneksi ke port localhost).
|
|
||||||
|
|
||||||
### Environment Variables Telemetry
|
|
||||||
|
|
||||||
| Variable | Default | Deskripsi |
|
|
||||||
|----------|---------|-----------|
|
|
||||||
| `CLICKHOUSE_USER` | `telemetry` | User ClickHouse |
|
|
||||||
| `CLICKHOUSE_PASSWORD` | `telemetry` | Password ClickHouse |
|
|
||||||
|
|
||||||
## Workflow Machine Learning
|
|
||||||
|
|
||||||
Detail lengkap tersedia di [`Machine_Learning/README.md`](Machine_Learning/README.md). Ringkasnya:
|
|
||||||
|
|
||||||
1. Unduh `dataset_1.zip`, `dataset_2.zip`, dan `dataset_3.zip` lalu letakkan di `Machine_Learning/`.
|
|
||||||
2. Jalankan preprocessing lokal:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd Machine_Learning
|
|
||||||
python preprocessing.py
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Upload `dataset.zip` ke Google Drive.
|
|
||||||
4. Jalankan `notebook.ipynb` di Google Colab dengan GPU.
|
|
||||||
5. Download model terbaik sebagai `best_model/best_model.keras`.
|
|
||||||
6. Ekspor model produksi:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python save_model.py
|
|
||||||
```
|
|
||||||
|
|
||||||
7. Konversi TensorFlow.js via CLI:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
|
|
||||||
tensorflowjs_converter \
|
|
||||||
--input_format=tf_saved_model \
|
|
||||||
--output_format=tfjs_graph_model \
|
|
||||||
--signature_name=serving_default \
|
|
||||||
--saved_model_tags=serve \
|
|
||||||
model/saved_model \
|
|
||||||
model/tfjs_model
|
|
||||||
```
|
|
||||||
|
|
||||||
Output utama pipeline ML:
|
|
||||||
|
|
||||||
| Path | Kegunaan |
|
|
||||||
|---|---|
|
|---|---|
|
||||||
| `Machine_Learning/dataset.zip` | Dataset siap upload ke Colab |
|
| `DATABASE_URL` | URL koneksi PostgreSQL |
|
||||||
| `Machine_Learning/best_model/best_model.keras` | Model Keras hasil training |
|
| `SESSION_SECRET` | Secret untuk session auth |
|
||||||
| `Machine_Learning/model/saved_model/` | TensorFlow SavedModel |
|
| `WEB_APP_URL` | URL frontend (untuk CORS) |
|
||||||
| `Machine_Learning/model/model.tflite` | Model untuk mobile/TFLite |
|
| `ML_SERVICE_URL` | URL layanan inferensi ML |
|
||||||
| `Machine_Learning/model/model.onnx` | Model untuk Rust ONNX Runtime |
|
|
||||||
| `Machine_Learning/model/tfjs_model/` | Model untuk TensorFlow.js |
|
|
||||||
|
|
||||||
## Artifact Lokal dan Generated Files
|
### Pipeline ML (Ringkasan)
|
||||||
|
|
||||||
Beberapa file tidak tersedia di fresh clone karena berukuran besar, dihasilkan lokal, atau berasal dari sumber eksternal:
|
1. Unduh 3 dataset ZIP → letakkan di `Machine_Learning/`
|
||||||
|
2. `python preprocessing.py` — gabungkan & bersihkan dataset
|
||||||
|
3. Upload `dataset.zip` ke Google Drive
|
||||||
|
4. Jalankan `notebook.ipynb` di Google Colab (GPU T4)
|
||||||
|
5. Download `best_model.keras`
|
||||||
|
6. `python save_model.py` → TFLite + SavedModel
|
||||||
|
7. Konversi ke TFJS & ONNX
|
||||||
|
|
||||||
- `Machine_Learning/dataset_1.zip`
|
> 📖 **Panduan lengkap:** [`Machine_Learning/README.md`](Machine_Learning/README.md)
|
||||||
- `Machine_Learning/dataset_2.zip`
|
|
||||||
- `Machine_Learning/dataset_3.zip`
|
|
||||||
- `Machine_Learning/dataset/`
|
|
||||||
- `Machine_Learning/dataset.zip`
|
|
||||||
- `Machine_Learning/best_model/best_model.keras`
|
|
||||||
- `Machine_Learning/model/saved_model/`
|
|
||||||
- `Machine_Learning/model/model.tflite`
|
|
||||||
- `Machine_Learning/model/model.onnx`
|
|
||||||
- `Machine_Learning/model/tfjs_model/`
|
|
||||||
|
|
||||||
## Environment Variable Penting
|
### Deployment
|
||||||
|
|
||||||
| Variable | Digunakan oleh | Keterangan |
|
```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 |
|
| **Web** | React + Vite → Static SPA | `bun run build` |
|
||||||
| `API_PORT` | API | Port backend produksi |
|
| **Android** | Tauri 2 + Rust → WebView APK | `cd apps/tauri && bun run tauri android build --apk` |
|
||||||
| `WEB_APP_URL` | API | URL frontend untuk konfigurasi CORS/integrasi |
|
|
||||||
| `ML_SERVICE_URL` | API | URL layanan ML |
|
|
||||||
| `MODEL_PATH` | ML Service | Lokasi file model ONNX, default `../../Machine_Learning/model/model.onnx` |
|
|
||||||
| `MODEL_INPUT_SIZE` | ML Service | Ukuran input model, default produksi `224` |
|
|
||||||
|
|
||||||
## Troubleshooting
|
### 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
|
```bash
|
||||||
bun install
|
# Development Android (butuh Android SDK + emulator/device)
|
||||||
|
cd apps/tauri
|
||||||
|
bun run tauri android init # Init project Android
|
||||||
|
bun run tauri android dev # Dev dengan hot reload
|
||||||
|
bun run tauri android build --apk # Build APK production
|
||||||
|
|
||||||
|
# CI/CD — dijalankan otomatis via GitHub Actions
|
||||||
|
.github/workflows/android.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
### API membutuhkan database
|
> Konfigurasi: `apps/tauri/tauri.conf.json` • `apps/tauri/gen/android/`
|
||||||
|
|
||||||
Pastikan `DATABASE_URL` tersedia di `.env` root dan PostgreSQL dapat diakses oleh aplikasi.
|
---
|
||||||
|
|
||||||
### ML service gagal memuat model
|
## 📚 Dokumentasi
|
||||||
|
|
||||||
Pastikan file model tersedia di path default:
|
| Dokumen | Isi |
|
||||||
|
|---|---|
|
||||||
|
| [`Machine_Learning/README.md`](Machine_Learning/README.md) | Pipeline ML lengkap — preprocessing, training Colab, ekspor TFLite/TFJS/ONNX |
|
||||||
|
| [`apps/ml-service/README.md`](apps/ml-service/README.md) | ML Inference Service — setup, endpoint API, konfigurasi |
|
||||||
|
| [`infra/README.md`](infra/README.md) | Arsitektur multi-VPS — diagram, GitHub Secrets, port, metrics flow |
|
||||||
|
| [`METRICS.md`](METRICS.md) | Daftar lengkap metrik Prometheus |
|
||||||
|
| `telemetry/` (submodule) | Source code telemetry stack |
|
||||||
|
|
||||||
```text
|
---
|
||||||
Machine_Learning/model/model.onnx
|
|
||||||
```
|
|
||||||
|
|
||||||
Atau set path khusus:
|
## 🔧 Troubleshooting
|
||||||
|
|
||||||
```bash
|
| Masalah | Solusi |
|
||||||
MODEL_PATH=/path/to/model.onnx cargo run
|
|---|---|
|
||||||
```
|
| `bun install` gagal | `bun --version` — pastikan ≥ 1.x |
|
||||||
|
| API perlu database | Isi `DATABASE_URL` di root `.env` |
|
||||||
|
| ML service gagal muat model | `ls Machine_Learning/model/model.onnx` — jalankan pipeline ML jika belum ada |
|
||||||
|
| Docker Compose gagal | `docker network create app-shared-net` |
|
||||||
|
| Konversi TFJS gagal | `export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python` |
|
||||||
|
|
||||||
### Docker Compose gagal karena network tidak ditemukan
|
---
|
||||||
|
|
||||||
`docker-compose.yml` menggunakan network eksternal `app-shared-net`. Buat network tersebut jika belum ada:
|
## 📖 Daftar Pustaka
|
||||||
|
|
||||||
```bash
|
1. Prayogi, A. et al. *"Klasifikasi Penyakit Daun Jagung Menggunakan CNN"* — [SISTEMATIS](https://ejournal.rizaniamedia.com/index.php/sistematis/article/view/87/49)
|
||||||
docker network create app-shared-net
|
2. Nugroho, A. et al. *"Deteksi Penyakit Daun Jagung dengan Deep Learning"* — [MIND Journal](https://ejurnal.itenas.ac.id/index.php/mindjournal/article/view/14032/4209)
|
||||||
```
|
3. Ramadhan, F. et al. *"Identifikasi Penyakit Jagung Berbasis Citra Digital"* — [Informa](https://www.informa.poltekindonusa.ac.id/index.php/informa/article/view/199/170)
|
||||||
|
4. Corteva Agriscience. *"Kenali Ragam Jenis Penyakit Jagung dan Cara Mengatasinya"* — [corteva.com](https://www.corteva.com/id/berita/Kenali-Ragam-Jenis-Penyakit-Jagung-dan-Cara-Mengatasinya.html)
|
||||||
|
|
||||||
### Konversi TensorFlow.js gagal karena konflik protobuf
|
---
|
||||||
|
|
||||||
Jalankan konversi melalui CLI dan set environment variable berikut:
|
<p align="center">
|
||||||
|
<sub>
|
||||||
```bash
|
Capstone Project • Pijak × IBM SkillsBuild • AI for Smart Education<br>
|
||||||
export PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=python
|
© 2026 ZeaVis Edu Team
|
||||||
```
|
</sub>
|
||||||
|
</p>
|
||||||
## Pengembangan
|
|
||||||
|
|
||||||
Alur umum pengembangan:
|
|
||||||
|
|
||||||
1. Install dependency dengan `bun install`.
|
|
||||||
2. Jalankan service yang dibutuhkan secara lokal.
|
|
||||||
3. Jalankan `bun run typecheck` sebelum membuat commit.
|
|
||||||
4. Jalankan `bun run build` untuk memverifikasi build produksi.
|
|
||||||
5. Untuk perubahan ML, ikuti dokumentasi detail di `Machine_Learning/README.md`.
|
|
||||||
6. Untuk perubahan ML service, cek juga `apps/ml-service/README.md`.
|
|
||||||
|
|
||||||
## Dokumentasi Terkait
|
|
||||||
|
|
||||||
- [`Machine_Learning/README.md`](Machine_Learning/README.md) — panduan lengkap dataset, training, dan ekspor model.
|
|
||||||
- [`apps/ml-service/README.md`](apps/ml-service/README.md) — panduan menjalankan dan memverifikasi layanan inferensi ML.
|
|
||||||
|
|||||||
+5
-2
@@ -4,8 +4,9 @@ WORKDIR /app
|
|||||||
COPY package.json bun.lock bunfig.toml tsconfig.base.json ./
|
COPY package.json bun.lock bunfig.toml tsconfig.base.json ./
|
||||||
COPY apps/api/package.json apps/api/package.json
|
COPY apps/api/package.json apps/api/package.json
|
||||||
COPY apps/web/package.json apps/web/package.json
|
COPY apps/web/package.json apps/web/package.json
|
||||||
|
COPY apps/tauri/package.json apps/tauri/package.json
|
||||||
COPY packages/shared/package.json packages/shared/package.json
|
COPY packages/shared/package.json packages/shared/package.json
|
||||||
RUN bun install --frozen-lockfile --production
|
RUN bun install --production
|
||||||
|
|
||||||
FROM oven/bun:1.3.14 AS runner
|
FROM oven/bun:1.3.14 AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -13,7 +14,9 @@ ENV NODE_ENV=production
|
|||||||
ENV API_PORT=3000
|
ENV API_PORT=3000
|
||||||
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY package.json bun.lock bunfig.toml tsconfig.base.json ./
|
COPY --from=deps /app/apps/api/node_modules apps/api/node_modules
|
||||||
|
COPY --from=deps /app/bun.lock ./bun.lock
|
||||||
|
COPY package.json bunfig.toml tsconfig.base.json ./
|
||||||
COPY apps/api apps/api
|
COPY apps/api apps/api
|
||||||
COPY packages/shared packages/shared
|
COPY packages/shared packages/shared
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ const googleOAuthEnabled = Boolean(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const webAppUrl = Bun.env.WEB_APP_URL ?? 'http://localhost:5173';
|
const webAppUrl = Bun.env.WEB_APP_URL ?? 'http://localhost:5173';
|
||||||
|
const allowedOrigins = [
|
||||||
|
webAppUrl,
|
||||||
|
'https://tauri.localhost',
|
||||||
|
'http://tauri.localhost',
|
||||||
|
'tauri://localhost',
|
||||||
|
'http://localhost:5173',
|
||||||
|
];
|
||||||
const secureCookies = Bun.env.SECURE_COOKIES === 'true' || webAppUrl.startsWith('https://');
|
const secureCookies = Bun.env.SECURE_COOKIES === 'true' || webAppUrl.startsWith('https://');
|
||||||
|
|
||||||
export const env = {
|
export const env = {
|
||||||
@@ -24,6 +31,7 @@ export const env = {
|
|||||||
googleClientSecret: Bun.env.GOOGLE_CLIENT_SECRET,
|
googleClientSecret: Bun.env.GOOGLE_CLIENT_SECRET,
|
||||||
googleRedirectUri: Bun.env.GOOGLE_REDIRECT_URI,
|
googleRedirectUri: Bun.env.GOOGLE_REDIRECT_URI,
|
||||||
webAppUrl,
|
webAppUrl,
|
||||||
|
allowedOrigins,
|
||||||
secureCookies,
|
secureCookies,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ assertRequiredEnv();
|
|||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
.use(cors({
|
.use(cors({
|
||||||
origin: env.webAppUrl,
|
origin: env.allowedOrigins,
|
||||||
credentials: true,
|
credentials: true,
|
||||||
}))
|
}))
|
||||||
.use(metricsRoutes)
|
.use(metricsRoutes)
|
||||||
|
|||||||
@@ -28,14 +28,26 @@ function hashToken(token: string) {
|
|||||||
return createHash('sha256').update(`${env.sessionSecret}:${token}`).digest('hex');
|
return createHash('sha256').update(`${env.sessionSecret}:${token}`).digest('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createSessionCookie(token: string) {
|
function isSecureRequest(headers?: { get(name: string): string | null }) {
|
||||||
|
if (env.secureCookies) return true;
|
||||||
|
// Detect HTTPS behind proxy (X-Forwarded-Proto)
|
||||||
|
const proto = headers?.get('x-forwarded-proto');
|
||||||
|
if (proto === 'https') return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSameSite(headers?: { get(name: string): string | null }) {
|
||||||
|
return isSecureRequest(headers) ? 'SameSite=None; Secure' : 'SameSite=Lax';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSessionCookie(token: string, headers?: { get(name: string): string | null }) {
|
||||||
const maxAge = 60 * 60 * 24 * 30;
|
const maxAge = 60 * 60 * 24 * 30;
|
||||||
const sameSite = env.secureCookies ? 'SameSite=None; Secure' : 'SameSite=Lax';
|
const sameSite = buildSameSite(headers);
|
||||||
return `${sessionCookieName}=${token}; HttpOnly; Path=/; ${sameSite}; Max-Age=${maxAge}`;
|
return `${sessionCookieName}=${token}; HttpOnly; Path=/; ${sameSite}; Max-Age=${maxAge}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearSessionCookie() {
|
export function clearSessionCookie(headers?: { get(name: string): string | null }) {
|
||||||
const sameSite = env.secureCookies ? 'SameSite=None; Secure' : 'SameSite=Lax';
|
const sameSite = buildSameSite(headers);
|
||||||
return `${sessionCookieName}=; HttpOnly; Path=/; ${sameSite}; Max-Age=0`;
|
return `${sessionCookieName}=; HttpOnly; Path=/; ${sameSite}; Max-Age=0`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +61,19 @@ export function readSessionToken(cookieHeader: string | null | undefined) {
|
|||||||
return decodeURIComponent(sessionCookie.slice(sessionCookieName.length + 1));
|
return decodeURIComponent(sessionCookie.slice(sessionCookieName.length + 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract bearer token from Authorization header.
|
||||||
|
* Used as fallback when cookies are blocked (e.g. Android WebView third-party blocking).
|
||||||
|
*/
|
||||||
|
export function readBearerToken(headers?: { get(name: string): string | null }) {
|
||||||
|
if (!headers) return null;
|
||||||
|
const auth = headers.get('authorization');
|
||||||
|
if (!auth) return null;
|
||||||
|
const parts = auth.split(' ');
|
||||||
|
if (parts.length !== 2 || parts[0] !== 'Bearer') return null;
|
||||||
|
return parts[1];
|
||||||
|
}
|
||||||
|
|
||||||
export async function createSession(userId: string) {
|
export async function createSession(userId: string) {
|
||||||
const db = createDbClient();
|
const db = createDbClient();
|
||||||
const token = randomBytes(32).toString('base64url');
|
const token = randomBytes(32).toString('base64url');
|
||||||
@@ -71,8 +96,12 @@ export async function deleteSession(token: string | null) {
|
|||||||
await db.delete(sessions).where(eq(sessions.tokenHash, hashToken(token)));
|
await db.delete(sessions).where(eq(sessions.tokenHash, hashToken(token)));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCurrentUser(cookieHeader: string | null | undefined): Promise<CurrentUser | null> {
|
export async function getCurrentUser(
|
||||||
const token = readSessionToken(cookieHeader);
|
cookieHeader: string | null | undefined,
|
||||||
|
headers?: { get(name: string): string | null },
|
||||||
|
): Promise<CurrentUser | null> {
|
||||||
|
// Try cookie first, then Authorization header (for Android WebView where 3rd-party cookies are blocked)
|
||||||
|
const token = readSessionToken(cookieHeader) ?? readBearerToken(headers);
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
|
|
||||||
const db = createDbClient();
|
const db = createDbClient();
|
||||||
|
|||||||
+201
-11
@@ -18,6 +18,103 @@ import {
|
|||||||
import { env } from '../config/env';
|
import { env } from '../config/env';
|
||||||
import { authCounter } from '../lib/telemetry';
|
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, '"');
|
||||||
|
const escapedPath = pathAndQuery.replace(/"/g, '"');
|
||||||
|
// intent:// scheme: Chrome on Android opens the target app by package name
|
||||||
|
// browser_fallback_url: shown if the app isn't installed
|
||||||
|
// Use intent://login/... to produce data URI zeavisedu://login/login?token=xxx
|
||||||
|
// which new URL() can parse (single-slash non-hierarchical URLs break WebView)
|
||||||
|
const intentUrl = `intent://login${escapedPath}#Intent;scheme=zeavisedu;package=com.zeavis.edu;S.browser_fallback_url=${encodeURIComponent(targetUrl)};end`;
|
||||||
|
|
||||||
|
const html = `<!DOCTYPE html>
|
||||||
|
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Kembali ke ZeaVis Edu</title></head>
|
||||||
|
<body style="font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f0fdf4">
|
||||||
|
<div style="text-align:center;padding:2rem;max-width:360px">
|
||||||
|
<p style="color:#166534;font-size:1.1rem;margin-bottom:1.5rem">Login Google berhasil!<br>Kembali ke aplikasi...</p>
|
||||||
|
<a href="${intentUrl.replace(/"/g, '"')}" id="open-app" style="display:inline-block;background:#16a34a;color:white;padding:0.75rem 2rem;border-radius:0.5rem;text-decoration:none;font-weight:600;font-size:1rem;margin-bottom:1rem">Buka ZeaVis Edu</a>
|
||||||
|
<p style="color:#6b7280;font-size:0.8rem">Jika tombol di atas tidak berfungsi, salin dan buka URL ini di aplikasi ZeaVis Edu:</p>
|
||||||
|
<code style="display:block;word-break:break-all;font-size:0.7rem;color:#4b5563;background:#e5e7eb;padding:0.5rem;border-radius:0.25rem;margin-top:0.5rem">${displayUrl.replace(/</g, '<').replace(/>/g, '>')}</code>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// Auto-open the intent
|
||||||
|
window.location.href = ${JSON.stringify(intentUrl)};
|
||||||
|
</script>
|
||||||
|
</body></html>`;
|
||||||
|
return new Response(html, {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'text/html;charset=utf-8' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePlatform(stateRaw: string | undefined): string {
|
||||||
|
try {
|
||||||
|
if (stateRaw) {
|
||||||
|
const parsed = JSON.parse(Buffer.from(stateRaw, 'base64url').toString('utf-8'));
|
||||||
|
return parsed.platform ?? 'web';
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
return 'web';
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeEmail(email: unknown) {
|
function normalizeEmail(email: unknown) {
|
||||||
return typeof email === 'string' ? email.trim().toLowerCase() : '';
|
return typeof email === 'string' ? email.trim().toLowerCase() : '';
|
||||||
}
|
}
|
||||||
@@ -32,13 +129,13 @@ function validatePassword(password: unknown) {
|
|||||||
|
|
||||||
export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
||||||
.get('/me', async ({ request }) => {
|
.get('/me', async ({ request }) => {
|
||||||
const user = await getCurrentUser(request.headers.get('cookie'));
|
const user = await getCurrentUser(request.headers.get('cookie'), request.headers);
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
features: getAuthFeatures(),
|
features: getAuthFeatures(),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.post('/register', async ({ body, set }) => {
|
.post('/register', async ({ body, set, request }) => {
|
||||||
const req = body as Partial<RegisterRequest> | undefined;
|
const req = body as Partial<RegisterRequest> | undefined;
|
||||||
const email = normalizeEmail(req?.email);
|
const email = normalizeEmail(req?.email);
|
||||||
const name = normalizeName(req?.name);
|
const name = normalizeName(req?.name);
|
||||||
@@ -62,7 +159,7 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
|||||||
|
|
||||||
const user = inserted[0];
|
const user = inserted[0];
|
||||||
const token = await createSession(user.id);
|
const token = await createSession(user.id);
|
||||||
set.headers['Set-Cookie'] = createSessionCookie(token);
|
set.headers['Set-Cookie'] = createSessionCookie(token, request.headers);
|
||||||
|
|
||||||
authCounter.labels('register', 'true').inc();
|
authCounter.labels('register', 'true').inc();
|
||||||
|
|
||||||
@@ -73,13 +170,14 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
role: 'user' as const,
|
role: 'user' as const,
|
||||||
},
|
},
|
||||||
|
token,
|
||||||
features: getAuthFeatures(),
|
features: getAuthFeatures(),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return serviceUnavailable('Database unavailable');
|
return serviceUnavailable('Database unavailable');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.post('/login', async ({ body, set }) => {
|
.post('/login', async ({ body, set, request }) => {
|
||||||
const req = body as Partial<AuthRequest> | undefined;
|
const req = body as Partial<AuthRequest> | undefined;
|
||||||
const email = normalizeEmail(req?.email);
|
const email = normalizeEmail(req?.email);
|
||||||
|
|
||||||
@@ -98,7 +196,7 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
|||||||
}
|
}
|
||||||
|
|
||||||
const token = await createSession(user.id);
|
const token = await createSession(user.id);
|
||||||
set.headers['Set-Cookie'] = createSessionCookie(token);
|
set.headers['Set-Cookie'] = createSessionCookie(token, request.headers);
|
||||||
|
|
||||||
authCounter.labels('login', 'true').inc();
|
authCounter.labels('login', 'true').inc();
|
||||||
|
|
||||||
@@ -109,6 +207,7 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
role: user.role === 'expert' ? 'expert' as const : 'user' as const,
|
role: user.role === 'expert' ? 'expert' as const : 'user' as const,
|
||||||
},
|
},
|
||||||
|
token,
|
||||||
features: getAuthFeatures(),
|
features: getAuthFeatures(),
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -116,16 +215,20 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.post('/logout', async ({ request, set }) => {
|
.post('/logout', async ({ request, set }) => {
|
||||||
await deleteSession(readSessionToken(request.headers.get('cookie')));
|
const cookieHeader = request.headers.get('cookie');
|
||||||
set.headers['Set-Cookie'] = clearSessionCookie();
|
await deleteSession(readSessionToken(cookieHeader));
|
||||||
|
set.headers['Set-Cookie'] = clearSessionCookie(request.headers);
|
||||||
return { ok: true };
|
return { ok: true };
|
||||||
})
|
})
|
||||||
.get('/google', ({ set }) => {
|
.get('/google', ({ query, set }) => {
|
||||||
if (!env.googleOAuthEnabled) {
|
if (!env.googleOAuthEnabled) {
|
||||||
set.status = 404;
|
set.status = 404;
|
||||||
return { error: 'Google OAuth is not configured' };
|
return { error: 'Google OAuth is not configured' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const platform = (query as Record<string, string>).platform ?? 'web';
|
||||||
|
const state = Buffer.from(JSON.stringify({ platform })).toString('base64url');
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
client_id: env.googleClientId!,
|
client_id: env.googleClientId!,
|
||||||
redirect_uri: env.googleRedirectUri!,
|
redirect_uri: env.googleRedirectUri!,
|
||||||
@@ -133,15 +236,102 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
|||||||
scope: 'openid email profile',
|
scope: 'openid email profile',
|
||||||
access_type: 'offline',
|
access_type: 'offline',
|
||||||
prompt: 'select_account',
|
prompt: 'select_account',
|
||||||
|
state,
|
||||||
});
|
});
|
||||||
|
|
||||||
set.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) {
|
if (!env.googleOAuthEnabled) {
|
||||||
set.status = 404;
|
set.status = 404;
|
||||||
return { error: 'Google OAuth is not configured' };
|
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;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const dashboardRoutes = new Elysia({ prefix: '/api/v1' })
|
|||||||
.get('/dashboard/summary', async ({ request }) => {
|
.get('/dashboard/summary', async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
const db = createDbClient();
|
const db = createDbClient();
|
||||||
const user = await getCurrentUser(request.headers.get('cookie'));
|
const user = await getCurrentUser(request.headers.get('cookie'), request.headers);
|
||||||
|
|
||||||
const diseases = await db.select().from(diseaseCatalog).orderBy(diseaseCatalog.displayOrder);
|
const diseases = await db.select().from(diseaseCatalog).orderBy(diseaseCatalog.displayOrder);
|
||||||
const manualRows = await db.select().from(manualClassifications).orderBy(desc(manualClassifications.createdAt)).limit(1);
|
const manualRows = await db.select().from(manualClassifications).orderBy(desc(manualClassifications.createdAt)).limit(1);
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ function getFileFromBody(body: unknown): File | null {
|
|||||||
|
|
||||||
export const diagnosisRoutes = new Elysia({ prefix: '/api/v1' })
|
export const diagnosisRoutes = new Elysia({ prefix: '/api/v1' })
|
||||||
.post('/diagnoses', async ({ body, request }) => {
|
.post('/diagnoses', async ({ body, request }) => {
|
||||||
const user = await getCurrentUser(request.headers.get('cookie'));
|
const user = await getCurrentUser(request.headers.get('cookie'), request.headers);
|
||||||
if (!user) return unauthorized('Authentication required');
|
if (!user) return unauthorized('Authentication required');
|
||||||
|
|
||||||
const file = getFileFromBody(body);
|
const file = getFileFromBody(body);
|
||||||
@@ -251,7 +251,7 @@ export const diagnosisRoutes = new Elysia({ prefix: '/api/v1' })
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.get('/diagnoses', async ({ request }) => {
|
.get('/diagnoses', async ({ request }) => {
|
||||||
const user = await getCurrentUser(request.headers.get('cookie'));
|
const user = await getCurrentUser(request.headers.get('cookie'), request.headers);
|
||||||
if (!user) return unauthorized('Authentication required');
|
if (!user) return unauthorized('Authentication required');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -270,7 +270,7 @@ export const diagnosisRoutes = new Elysia({ prefix: '/api/v1' })
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.get('/diagnoses/:id', async ({ params, request }) => {
|
.get('/diagnoses/:id', async ({ params, request }) => {
|
||||||
const user = await getCurrentUser(request.headers.get('cookie'));
|
const user = await getCurrentUser(request.headers.get('cookie'), request.headers);
|
||||||
if (!user) return unauthorized('Authentication required');
|
if (!user) return unauthorized('Authentication required');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function isDiagnosisRecordOrNull(record: unknown): record is DiagnosisRecord {
|
|||||||
|
|
||||||
export const expertRoutes = new Elysia({ prefix: '/api/v1/expert' })
|
export const expertRoutes = new Elysia({ prefix: '/api/v1/expert' })
|
||||||
.get('/reviews', async ({ request }) => {
|
.get('/reviews', async ({ request }) => {
|
||||||
const user = await getCurrentUser(request.headers.get('cookie'));
|
const user = await getCurrentUser(request.headers.get('cookie'), request.headers);
|
||||||
if (!user) return unauthorized('Authentication required');
|
if (!user) return unauthorized('Authentication required');
|
||||||
if (user.role !== 'expert') return forbidden('Expert role required');
|
if (user.role !== 'expert') return forbidden('Expert role required');
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ export const expertRoutes = new Elysia({ prefix: '/api/v1/expert' })
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.post('/reviews/:diagnosisId', async ({ params, body, request }) => {
|
.post('/reviews/:diagnosisId', async ({ params, body, request }) => {
|
||||||
const user = await getCurrentUser(request.headers.get('cookie'));
|
const user = await getCurrentUser(request.headers.get('cookie'), request.headers);
|
||||||
if (!user) return unauthorized('Authentication required');
|
if (!user) return unauthorized('Authentication required');
|
||||||
if (user.role !== 'expert') return forbidden('Expert role required');
|
if (user.role !== 'expert') return forbidden('Expert role required');
|
||||||
|
|
||||||
|
|||||||
+97
-163
@@ -1,8 +1,25 @@
|
|||||||
# ML Service — Rust Axum ONNX Runtime
|
# ML Inference Service — ZeaVis Edu
|
||||||
|
|
||||||
Layanan inferensi machine learning berbasis Rust dengan Axum web framework dan ONNX Runtime untuk klasifikasi penyakit daun jagung. Service ini menyediakan endpoint HTTP untuk prediksi real-time dengan performa tinggi dan konsumsi resource minimal.
|
> Layanan inferensi machine learning berbasis Rust/Axum + ONNX Runtime untuk klasifikasi penyakit daun jagung.
|
||||||
|
|
||||||
## Fitur
|
← [Kembali ke README utama](../../README.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Daftar Isi
|
||||||
|
|
||||||
|
1. [Fitur](#1-fitur)
|
||||||
|
2. [Prasyarat & Instalasi](#2-prasyarat--instalasi)
|
||||||
|
3. [Menjalankan Service](#3-menjalankan-service)
|
||||||
|
4. [Environment Variables](#4-environment-variables)
|
||||||
|
5. [Endpoint API](#5-endpoint-api)
|
||||||
|
6. [Verifikasi & Testing](#6-verifikasi--testing)
|
||||||
|
7. [Docker Deployment](#7-docker-deployment)
|
||||||
|
8. [Troubleshooting](#8-troubleshooting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Fitur
|
||||||
|
|
||||||
- **Framework:** Axum (async Rust web framework)
|
- **Framework:** Axum (async Rust web framework)
|
||||||
- **Runtime Inferensi:** ONNX Runtime untuk kompatibilitas lintas platform
|
- **Runtime Inferensi:** ONNX Runtime untuk kompatibilitas lintas platform
|
||||||
@@ -10,15 +27,13 @@ Layanan inferensi machine learning berbasis Rust dengan Axum web framework dan O
|
|||||||
- **Endpoint:** Health check, metadata, dan prediksi gambar
|
- **Endpoint:** Health check, metadata, dan prediksi gambar
|
||||||
- **Multipart Upload:** Dukungan upload gambar langsung via HTTP POST
|
- **Multipart Upload:** Dukungan upload gambar langsung via HTTP POST
|
||||||
|
|
||||||
## Prasyarat
|
---
|
||||||
|
|
||||||
|
## 2. Prasyarat & Instalasi
|
||||||
|
|
||||||
- Rust 1.70+ dan Cargo
|
- Rust 1.70+ dan Cargo
|
||||||
- Model ONNX di `../../Machine_Learning/model/model.onnx` (atau path custom via `MODEL_PATH`)
|
- Model ONNX di `../../Machine_Learning/model/model.onnx` (atau path custom via `MODEL_PATH`)
|
||||||
|
|
||||||
## Instalasi & Setup
|
|
||||||
|
|
||||||
### Instalasi Dependensi
|
|
||||||
|
|
||||||
Dependensi Rust sudah terdaftar di `Cargo.toml`. Cargo akan mengunduh dan mengkompilasi otomatis saat pertama kali build.
|
Dependensi Rust sudah terdaftar di `Cargo.toml`. Cargo akan mengunduh dan mengkompilasi otomatis saat pertama kali build.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -27,75 +42,58 @@ cargo build
|
|||||||
|
|
||||||
Output build lokal berada di `target/` dan direktori tersebut diabaikan oleh Git.
|
Output build lokal berada di `target/` dan direktori tersebut diabaikan oleh Git.
|
||||||
|
|
||||||
## Menjalankan Service Lokal
|
---
|
||||||
|
|
||||||
|
## 3. Menjalankan Service
|
||||||
|
|
||||||
Semua perintah di bawah dijalankan dari direktori `apps/ml-service`.
|
Semua perintah di bawah dijalankan dari direktori `apps/ml-service`.
|
||||||
|
|
||||||
### Opsi 1: Default (Port 8000, Model dari Machine_Learning/)
|
### Opsi 1: Default (Port 8000)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd apps/ml-service
|
|
||||||
cargo run
|
cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
Service akan mencari model di path default dan mendengarkan di `http://localhost:8000`:
|
Service akan mencari model di path default:
|
||||||
```
|
```
|
||||||
../../Machine_Learning/model/model.onnx
|
../../Machine_Learning/model/model.onnx
|
||||||
```
|
```
|
||||||
|
|
||||||
### Opsi 2: Local Development dengan .env.example (Port 8001)
|
### Opsi 2: Local Development dengan .env.example (Port 8001)
|
||||||
|
|
||||||
Untuk development lokal dengan port 8001 (sesuai `.env.example`):
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd apps/ml-service
|
|
||||||
source .env.example
|
source .env.example
|
||||||
cargo run
|
cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
Service akan mendengarkan di `http://localhost:8001` karena `ML_SERVICE_PORT=8001` di `.env.example`.
|
### Opsi 3: Custom Model Path & Port
|
||||||
|
|
||||||
### Opsi 3: Custom Model Path
|
|
||||||
|
|
||||||
Jika model berada di lokasi lain, gunakan environment variable `MODEL_PATH`:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd apps/ml-service
|
|
||||||
MODEL_PATH=/path/to/model.onnx cargo run
|
|
||||||
```
|
|
||||||
|
|
||||||
Atau kombinasikan dengan port custom:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd apps/ml-service
|
|
||||||
ML_SERVICE_PORT=9000 MODEL_PATH=/path/to/model.onnx cargo run
|
ML_SERVICE_PORT=9000 MODEL_PATH=/path/to/model.onnx cargo run
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment Variables
|
---
|
||||||
|
|
||||||
|
## 4. Environment Variables
|
||||||
|
|
||||||
| Variable | Default | Keterangan |
|
| Variable | Default | Keterangan |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `ML_SERVICE_HOST` | `0.0.0.0` | Bind address |
|
| `ML_SERVICE_HOST` | `0.0.0.0` | Bind address |
|
||||||
| `ML_SERVICE_PORT` | `8000` | Bind port (override untuk local dev dengan `.env.example`) |
|
| `ML_SERVICE_PORT` | `8000` | Bind port |
|
||||||
| `MODEL_PATH` | `../../Machine_Learning/model/model.onnx` | Path ke file model ONNX |
|
| `MODEL_PATH` | `../../Machine_Learning/model/model.onnx` | Path ke file model ONNX |
|
||||||
| `MODEL_INPUT_SIZE` | `224` | Ukuran input gambar (224x224 untuk EfficientNetV2B0) |
|
| `MODEL_INPUT_SIZE` | `224` | Ukuran input gambar (224×224 untuk EfficientNetV2B0) |
|
||||||
| `RUST_LOG` | `info` | Level logging (debug, info, warn, error) |
|
| `RUST_LOG` | `info` | Level logging (debug, info, warn, error) |
|
||||||
|
|
||||||
## Endpoint API
|
---
|
||||||
|
|
||||||
### 1. Health Check
|
## 5. Endpoint API
|
||||||
|
|
||||||
|
### Health Check
|
||||||
|
|
||||||
**Default (port 8000):**
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:8000/health
|
curl http://localhost:8000/health
|
||||||
```
|
```
|
||||||
|
|
||||||
**Local dev dengan .env.example (port 8001):**
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8001/health
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
@@ -103,19 +101,12 @@ curl http://localhost:8001/health
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Metadata
|
### Metadata
|
||||||
|
|
||||||
**Default (port 8000):**
|
|
||||||
```bash
|
```bash
|
||||||
curl http://localhost:8000/metadata
|
curl http://localhost:8000/metadata
|
||||||
```
|
```
|
||||||
|
|
||||||
**Local dev dengan .env.example (port 8001):**
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8001/metadata
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"service_name": "zeavis-ml-service",
|
"service_name": "zeavis-ml-service",
|
||||||
@@ -123,32 +114,19 @@ curl http://localhost:8001/metadata
|
|||||||
"model_path": "../../Machine_Learning/model/model.onnx",
|
"model_path": "../../Machine_Learning/model/model.onnx",
|
||||||
"model_loaded": true,
|
"model_loaded": true,
|
||||||
"input_size": 224,
|
"input_size": 224,
|
||||||
"labels": [
|
"labels": ["Bercak Daun", "Daun Sehat", "Karat Daun", "Hawar Daun"]
|
||||||
"Bercak Daun",
|
|
||||||
"Daun Sehat",
|
|
||||||
"Karat Daun",
|
|
||||||
"Hawar Daun"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Prediksi
|
### Prediksi
|
||||||
|
|
||||||
Upload gambar daun jagung untuk klasifikasi:
|
Upload gambar daun jagung untuk klasifikasi:
|
||||||
|
|
||||||
**Default (port 8000):**
|
|
||||||
```bash
|
```bash
|
||||||
curl -X POST http://localhost:8000/predict \
|
curl -X POST http://localhost:8000/predict \
|
||||||
-F "file=@/path/to/corn-leaf.jpg"
|
-F "file=@/path/to/corn-leaf.jpg"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Local dev dengan .env.example (port 8001):**
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8001/predict \
|
|
||||||
-F "file=@/path/to/corn-leaf.jpg"
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"label": "Daun Sehat",
|
"label": "Daun Sehat",
|
||||||
@@ -162,120 +140,45 @@ curl -X POST http://localhost:8001/predict \
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Verifikasi & Testing
|
---
|
||||||
|
|
||||||
|
## 6. Verifikasi & Testing
|
||||||
|
|
||||||
### Build Produksi
|
### Build Produksi
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
# Binary di target/release/zeavis-ml-service
|
||||||
```
|
```
|
||||||
|
|
||||||
Output binary akan tersedia di `target/release/zeavis-ml-service`.
|
|
||||||
|
|
||||||
### Menjalankan Tests
|
### Menjalankan Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
|
|
||||||
Tests mencakup validasi loading model, preprocessing gambar, dan output prediksi.
|
### Verifikasi Manual (default port 8000)
|
||||||
|
|
||||||
### Verifikasi Manual
|
|
||||||
|
|
||||||
#### Dengan default port 8000:
|
|
||||||
|
|
||||||
1. Jalankan service:
|
|
||||||
```bash
|
|
||||||
cargo run
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Di terminal lain, test health endpoint:
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8000/health
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Test metadata:
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8000/metadata
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Test prediksi dengan gambar sample:
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8000/predict \
|
|
||||||
-F "file=@../../Machine_Learning/dataset/Daun\ Sehat/sample.jpg"
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Dengan local dev port 8001 (.env.example):
|
|
||||||
|
|
||||||
1. Jalankan service dengan .env.example:
|
|
||||||
```bash
|
|
||||||
source .env.example
|
|
||||||
cargo run
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Di terminal lain, test health endpoint:
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8001/health
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Test metadata:
|
|
||||||
```bash
|
|
||||||
curl http://localhost:8001/metadata
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Test prediksi dengan gambar sample:
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:8001/predict \
|
|
||||||
-F "file=@../../Machine_Learning/dataset/Daun\ Sehat/sample.jpg"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Model tidak ditemukan
|
|
||||||
|
|
||||||
**Error:** `Failed to load model: No such file or directory`
|
|
||||||
|
|
||||||
**Solusi:** Pastikan file model tersedia di path yang benar:
|
|
||||||
```bash
|
|
||||||
ls -la ../../Machine_Learning/model/model.onnx
|
|
||||||
```
|
|
||||||
|
|
||||||
Atau set path custom:
|
|
||||||
```bash
|
|
||||||
MODEL_PATH=/absolute/path/to/model.onnx cargo run
|
|
||||||
```
|
|
||||||
|
|
||||||
### Port sudah digunakan
|
|
||||||
|
|
||||||
**Error:** `Address already in use`
|
|
||||||
|
|
||||||
**Solusi:** Service menggunakan port 8000 secara default. Jika port sudah digunakan, ubah dengan environment variable:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ML_SERVICE_PORT=9000 cargo run
|
# 1. Start service
|
||||||
|
cargo run
|
||||||
|
|
||||||
|
# 2. Health check
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
|
# 3. Metadata
|
||||||
|
curl http://localhost:8000/metadata
|
||||||
|
|
||||||
|
# 4. Prediksi
|
||||||
|
curl -X POST http://localhost:8000/predict \
|
||||||
|
-F "file=@../../Machine_Learning/dataset/Daun\ Sehat/sample.jpg"
|
||||||
```
|
```
|
||||||
|
|
||||||
Atau jika menggunakan `.env.example` (port 8001), pastikan tidak ada service lain di port tersebut:
|
---
|
||||||
|
|
||||||
```bash
|
## 7. Docker Deployment
|
||||||
lsof -i :8001
|
|
||||||
```
|
|
||||||
|
|
||||||
### ONNX Runtime tidak kompatibel
|
Service dapat di-deploy via Docker. Build dari root repository karena Dockerfile menyalin source service dan artifact ONNX dari beberapa direktori repo.
|
||||||
|
|
||||||
**Error:** `ONNX Runtime initialization failed`
|
|
||||||
|
|
||||||
**Solusi:** Pastikan ONNX Runtime binary kompatibel dengan sistem operasi. Cargo akan mengunduh binary yang sesuai otomatis. Jika masalah persisten, coba rebuild:
|
|
||||||
```bash
|
|
||||||
cargo clean
|
|
||||||
cargo build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
Service dapat di-deploy via Docker. Jalankan build dari root repository karena Dockerfile menyalin source service dan artifact ONNX dari beberapa direktori repo.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -f apps/ml-service/Dockerfile -t zeavis-ml-service .
|
docker build -f apps/ml-service/Dockerfile -t zeavis-ml-service .
|
||||||
@@ -284,11 +187,42 @@ docker run -p 8000:8000 zeavis-ml-service
|
|||||||
|
|
||||||
Pastikan `Machine_Learning/model/model.onnx` sudah dibuat sebelum build image.
|
Pastikan `Machine_Learning/model/model.onnx` sudah dibuat sebelum build image.
|
||||||
|
|
||||||
### Docker Compose
|
---
|
||||||
|
|
||||||
Lihat `docker-compose.yml` di root repository untuk deployment lengkap dengan web, API, dan ML service.
|
## 8. Troubleshooting
|
||||||
|
|
||||||
## Dokumentasi Terkait
|
### Model tidak ditemukan
|
||||||
|
|
||||||
- [`Machine_Learning/README.md`](../../Machine_Learning/README.md) — Panduan training dan ekspor model ONNX
|
**Error:** `Failed to load model: No such file or directory`
|
||||||
- [`README.md`](../../README.md) — Dokumentasi proyek utama
|
|
||||||
|
**Solusi:**
|
||||||
|
```bash
|
||||||
|
ls -la ../../Machine_Learning/model/model.onnx
|
||||||
|
# Atau set path custom:
|
||||||
|
MODEL_PATH=/absolute/path/to/model.onnx cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
### Port sudah digunakan
|
||||||
|
|
||||||
|
**Error:** `Address already in use`
|
||||||
|
|
||||||
|
**Solusi:**
|
||||||
|
```bash
|
||||||
|
ML_SERVICE_PORT=9000 cargo run
|
||||||
|
# Cek port yang digunakan:
|
||||||
|
lsof -i :8000
|
||||||
|
```
|
||||||
|
|
||||||
|
### ONNX Runtime tidak kompatibel
|
||||||
|
|
||||||
|
**Error:** `ONNX Runtime initialization failed`
|
||||||
|
|
||||||
|
**Solusi:** Pastikan binary ONNX Runtime kompatibel dengan sistem operasi. Jika masalah persisten:
|
||||||
|
```bash
|
||||||
|
cargo clean
|
||||||
|
cargo build
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
← [Kembali ke README utama](../../README.md) • [Pipeline ML →](../../Machine_Learning/README.md) • [Infra →](../../infra/README.md)
|
||||||
|
|||||||
Generated
+3
@@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
+1870
File diff suppressed because it is too large
Load Diff
Generated
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DeviceTable">
|
||||||
|
<option name="columnSorters">
|
||||||
|
<list>
|
||||||
|
<ColumnSorterState>
|
||||||
|
<option name="column" value="Name" />
|
||||||
|
<option name="order" value="ASCENDING" />
|
||||||
|
</ColumnSorterState>
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+17
@@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DiscordProjectSettings">
|
||||||
|
<option name="show" value="ASK" />
|
||||||
|
<option name="description" value="" />
|
||||||
|
<option name="applicationTheme" value="default" />
|
||||||
|
<option name="iconsTheme" value="default" />
|
||||||
|
<option name="button1Title" value="" />
|
||||||
|
<option name="button1Url" value="" />
|
||||||
|
<option name="button2Title" value="" />
|
||||||
|
<option name="button2Url" value="" />
|
||||||
|
<option name="customApplicationId" value="" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/tauri.iml" filepath="$PROJECT_DIR$/.idea/tauri.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+9
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/../.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -12,5 +12,7 @@ tauri-build = { version = "2", features = [] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2", default-features = false, features = ["wry", "common-controls-v6", "dynamic-acl", "x11", "dbus", "custom-protocol"] }
|
tauri = { version = "2", default-features = false, features = ["wry", "common-controls-v6", "dynamic-acl", "x11", "dbus", "custom-protocol"] }
|
||||||
|
tauri-plugin-opener = "2"
|
||||||
|
tauri-plugin-deep-link = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
"description": "Capability for the main window",
|
"description": "Capability for the main window",
|
||||||
"windows": ["main"],
|
"windows": ["main"],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"core:default"
|
"core:default",
|
||||||
|
"opener:default",
|
||||||
|
"opener:allow-open-url",
|
||||||
|
"deep-link:default"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,5 +12,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2"
|
"@tauri-apps/cli": "^2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"sharp": "0.35.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Generate Android launcher icons from the ZeaVis Edu logo SVG.
|
||||||
|
* Produces PNGs at all required densities and replaces Tauri's default icons.
|
||||||
|
*
|
||||||
|
* Usage: node scripts/generate-icons.js
|
||||||
|
* Requires: bun add sharp (already in devDependencies)
|
||||||
|
*/
|
||||||
|
|
||||||
|
const sharp = require('sharp');
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const LOGO = path.resolve(__dirname, '../../../.github/assets/zeavis-logo.svg');
|
||||||
|
const RES = path.resolve(__dirname, '../gen/android/app/src/main/res');
|
||||||
|
|
||||||
|
// Android density buckets: [folder, size]
|
||||||
|
const DENSITIES = [
|
||||||
|
['mipmap-mdpi', 48],
|
||||||
|
['mipmap-hdpi', 72],
|
||||||
|
['mipmap-xhdpi', 96],
|
||||||
|
['mipmap-xxhdpi', 144],
|
||||||
|
['mipmap-xxxhdpi', 192],
|
||||||
|
];
|
||||||
|
|
||||||
|
async function generate() {
|
||||||
|
if (!fs.existsSync(LOGO)) {
|
||||||
|
console.error(`ERROR: Logo not found at ${LOGO}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Generating icons from ${LOGO}...`);
|
||||||
|
|
||||||
|
for (const [folder, size] of DENSITIES) {
|
||||||
|
const dir = path.join(RES, folder);
|
||||||
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||||
|
|
||||||
|
const png = await sharp(LOGO)
|
||||||
|
.resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
// Write both legacy and adaptive icon names
|
||||||
|
for (const name of ['ic_launcher.png', 'ic_launcher_foreground.png', 'ic_launcher_round.png']) {
|
||||||
|
fs.writeFileSync(path.join(dir, name), png);
|
||||||
|
}
|
||||||
|
console.log(` ${folder}: ${size}x${size} OK`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also write the legacy icon to drawable for completeness
|
||||||
|
const drawableDir = path.join(RES, 'drawable');
|
||||||
|
if (!fs.existsSync(drawableDir)) fs.mkdirSync(drawableDir, { recursive: true });
|
||||||
|
const refPng = await sharp(LOGO)
|
||||||
|
.resize(144, 144, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
fs.writeFileSync(path.join(drawableDir, 'ic_launcher.png'), refPng);
|
||||||
|
|
||||||
|
console.log('Done. Android launcher icons generated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
generate().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Executable
+45
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# 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
|
||||||
|
|
||||||
|
MANIFEST="gen/android/app/src/main/AndroidManifest.xml"
|
||||||
|
|
||||||
|
if [ ! -f "$MANIFEST" ]; then
|
||||||
|
echo "ERROR: $MANIFEST not found. Run 'tauri android init' first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── CAMERA permission ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if ! grep -q 'android.permission.CAMERA' "$MANIFEST"; then
|
||||||
|
echo "Adding CAMERA permission to AndroidManifest.xml..."
|
||||||
|
sed -i 's|<uses-permission android:name="android.permission.INTERNET" />|<uses-permission android:name="android.permission.INTERNET" />\n <uses-permission android:name="android.permission.CAMERA" />\n <uses-feature android:name="android.hardware.camera" android:required="false" />\n <uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />|' "$MANIFEST"
|
||||||
|
else
|
||||||
|
echo "CAMERA permission already present."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Deep link intent filter ────────────────────────────────────────────
|
||||||
|
# Allows the app to receive zeavisedu:// scheme URLs from the system browser
|
||||||
|
# (used after Google OAuth completes in external browser on Android)
|
||||||
|
|
||||||
|
DEEP_LINK_FILTER='<!-- Deep link for Google OAuth return from system browser -->\
|
||||||
|
<intent-filter android:autoVerify="true">\
|
||||||
|
<action android:name="android.intent.action.VIEW" />\
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />\
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />\
|
||||||
|
<data android:scheme="zeavisedu" />\
|
||||||
|
</intent-filter>'
|
||||||
|
|
||||||
|
if grep -q 'android:scheme="zeavisedu"' "$MANIFEST"; then
|
||||||
|
echo "Deep link intent filter already present."
|
||||||
|
else
|
||||||
|
echo "Adding deep link intent filter to AndroidManifest.xml..."
|
||||||
|
# Insert before the closing </activity> tag of MainActivity
|
||||||
|
sed -i "s|</activity>|${DEEP_LINK_FILTER}\n </activity>|" "$MANIFEST"
|
||||||
|
echo "Deep link intent filter added."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "AndroidManifest patched successfully."
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"beforeDevCommand": "cd ../web && bun run dev"
|
"beforeDevCommand": "cd ../web && bun run dev"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"withGlobalTauri": false,
|
"withGlobalTauri": true,
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "ZeaVis Edu",
|
"title": "ZeaVis Edu",
|
||||||
@@ -26,5 +26,13 @@
|
|||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all"
|
"targets": "all"
|
||||||
},
|
},
|
||||||
"plugins": {}
|
"plugins": {
|
||||||
|
"deep-link": {
|
||||||
|
"mobile": [
|
||||||
|
{
|
||||||
|
"scheme": ["zeavisedu"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,8 @@ COPY package.json bun.lock bunfig.toml tsconfig.base.json ./
|
|||||||
COPY packages/shared/package.json packages/shared/package.json
|
COPY packages/shared/package.json packages/shared/package.json
|
||||||
COPY apps/web/package.json apps/web/package.json
|
COPY apps/web/package.json apps/web/package.json
|
||||||
COPY apps/api/package.json apps/api/package.json
|
COPY apps/api/package.json apps/api/package.json
|
||||||
RUN bun install --frozen-lockfile
|
COPY apps/tauri/package.json apps/tauri/package.json
|
||||||
|
RUN bun install
|
||||||
|
|
||||||
COPY packages/shared packages/shared
|
COPY packages/shared packages/shared
|
||||||
COPY apps/web apps/web
|
COPY apps/web apps/web
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>ZeaVis Edu</title>
|
||||||
|
<script>
|
||||||
|
var LIVE = 'https://zeavisedu.asepharyana.my.id';
|
||||||
|
var T = window.__TAURI_INTERNALS__;
|
||||||
|
|
||||||
|
function navigate(path) {
|
||||||
|
window.location.replace(LIVE + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// On cold start, check if the app was opened via a deep link (Google OAuth)
|
||||||
|
// before redirecting to the live web app.
|
||||||
|
if (T && T.invoke) {
|
||||||
|
T.invoke('plugin:deep-link|get_current')
|
||||||
|
.then(function(urls) {
|
||||||
|
if (urls && urls.length > 0 && urls[0]) {
|
||||||
|
try {
|
||||||
|
var u = new URL(urls[0]);
|
||||||
|
var target = u.pathname + u.search + u.hash;
|
||||||
|
if (target && target !== '/') {
|
||||||
|
// Preserve full path + query (e.g. /login?token=xxx)
|
||||||
|
navigate(target);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) { /* malformed URL — fall through */ }
|
||||||
|
}
|
||||||
|
// No deep link — redirect to live app home
|
||||||
|
navigate('/');
|
||||||
|
})
|
||||||
|
.catch(function() { navigate('/'); });
|
||||||
|
} else {
|
||||||
|
// Not in Tauri (dev mode or unknown) — redirect to live
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body style="background:#f0fdf4;font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0">
|
||||||
|
<p style="color:#16a34a">Memuat ZeaVis Edu...</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -12,6 +12,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tanstack/react-query": "^5.100.11",
|
"@tanstack/react-query": "^5.100.11",
|
||||||
|
"@tauri-apps/plugin-deep-link": "^2",
|
||||||
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"@zeavis/shared": "workspace:*",
|
"@zeavis/shared": "workspace:*",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
"react-router-dom": "^7.15.1",
|
"react-router-dom": "^7.15.1",
|
||||||
"recharts": "3.8.1",
|
"recharts": "3.8.1",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"tailwindcss": "^4.3.1",
|
||||||
"web-vitals": "5.3.0",
|
"web-vitals": "5.3.0",
|
||||||
"zustand": "^5.0.13"
|
"zustand": "^5.0.13"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { MainLayout } from "@/components/layout/main-layout";
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useAuthStore } from "@/store/auth-store";
|
import { useAuthStore } from "@/store/auth-store";
|
||||||
import { apiClient } from "@/lib/api-client";
|
import { apiClient } from "@/lib/api-client";
|
||||||
|
import { setupDeepLinkHandler } from "@/lib/tauri";
|
||||||
|
|
||||||
function LogoutProses() {
|
function LogoutProses() {
|
||||||
const setUser = useAuthStore((state) => state.setUser);
|
const setUser = useAuthStore((state) => state.setUser);
|
||||||
@@ -161,6 +162,11 @@ function GlobalErrorTracker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
|
// Register deep link handler for Android OAuth return
|
||||||
|
useEffect(() => {
|
||||||
|
setupDeepLinkHandler();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthInitializer />
|
<AuthInitializer />
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 103 KiB |
@@ -1,9 +1,10 @@
|
|||||||
import { FormEvent, useState } from 'react';
|
import { FormEvent, useState, useCallback } from 'react';
|
||||||
import { Eye, EyeOff } from 'lucide-react';
|
import { Eye, EyeOff } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { isTauri, openUrl } from '@/lib/tauri';
|
||||||
|
|
||||||
type AuthFormProps = {
|
type AuthFormProps = {
|
||||||
mode: 'login' | 'register';
|
mode: 'login' | 'register';
|
||||||
@@ -25,13 +26,23 @@ export function AuthForm({ mode, isSubmitting, error, googleOAuthEnabled, onSubm
|
|||||||
await onSubmit({ name, email, password });
|
await onSubmit({ name, email, password });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleGoogleLogin = useCallback(async (e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const platform = isTauri() ? 'tauri' : 'web';
|
||||||
|
// Use API base URL, not window.location.origin — on Tauri Android
|
||||||
|
// the origin is http://tauri.localhost which is not the API server.
|
||||||
|
const apiBase = import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
||||||
|
const googleUrl = `${apiBase}/api/v1/auth/google?platform=${platform}`;
|
||||||
|
await openUrl(googleUrl);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="mx-auto w-full max-w-md">
|
<Card className="mx-auto w-full max-w-md bg-transparent border-none shadow-none">
|
||||||
<CardHeader>
|
<CardHeader className="text-center space-y-2">
|
||||||
<CardTitle>{mode === 'login' ? 'Masuk ke ZeaVis Edu' : 'Buat akun ZeaVis Edu'}</CardTitle>
|
<CardTitle className="text-2xl font-bold text-emerald-900">{mode === 'login' ? 'Masuk Akun ZeaVis Edu' : 'Buat akun ZeaVis Edu'}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription className="text-sm text-emerald-800/80">
|
||||||
{mode === 'login'
|
{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.'}
|
: 'Daftar untuk menyimpan diagnosis dan mengikuti review pakar.'}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -75,8 +86,15 @@ export function AuthForm({ mode, isSubmitting, error, googleOAuthEnabled, onSubm
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
{googleOAuthEnabled && (
|
{googleOAuthEnabled && (
|
||||||
<Button className="mt-3 w-full" variant="outline" asChild>
|
<Button className="mt-3 w-full flex items-center justify-center gap-2.5" variant="outline" onClick={handleGoogleLogin} type="button">
|
||||||
<a href="/api/v1/auth/google">Masuk dengan Google</a>
|
<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>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -10,27 +10,32 @@ type AuthGuardProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function AuthGuard({ children, requireExpert = false }: AuthGuardProps) {
|
export function AuthGuard({ children, requireExpert = false }: AuthGuardProps) {
|
||||||
|
const user = useAuthStore((state) => state.user);
|
||||||
const setUser = useAuthStore((state) => state.setUser);
|
const setUser = useAuthStore((state) => state.setUser);
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['auth', 'me'],
|
queryKey: ['auth', 'me'],
|
||||||
queryFn: () => apiClient.getMe(),
|
queryFn: () => apiClient.getMe(),
|
||||||
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query.data) {
|
if (query.data?.user) {
|
||||||
setUser(query.data.user);
|
setUser(query.data.user);
|
||||||
}
|
}
|
||||||
}, [query.data, setUser]);
|
}, [query.data, setUser]);
|
||||||
|
|
||||||
if (query.isLoading) {
|
// Tunjukkan loading hanya jika belum ada user di store
|
||||||
|
if (query.isLoading && !user) {
|
||||||
return <main className="min-h-screen p-8 text-center text-muted-foreground">Memeriksa sesi...</main>;
|
return <main className="min-h-screen p-8 text-center text-muted-foreground">Memeriksa sesi...</main>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!query.data?.user) {
|
// Cek store dulu, baru query — mencegah redirect saat refetch background
|
||||||
|
const currentUser = query.data?.user ?? user;
|
||||||
|
if (!currentUser) {
|
||||||
return <Navigate to="/login" replace />;
|
return <Navigate to="/login" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requireExpert && query.data.user.role !== 'expert') {
|
if (requireExpert && currentUser.role !== 'expert') {
|
||||||
return <Navigate to="/dashboard" replace />;
|
return <Navigate to="/dashboard" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ export function AuthInitializer() {
|
|||||||
queryKey: ['auth', 'me'],
|
queryKey: ['auth', 'me'],
|
||||||
queryFn: () => apiClient.getMe(),
|
queryFn: () => apiClient.getMe(),
|
||||||
retry: false,
|
retry: false,
|
||||||
|
staleTime: 30_000,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query.data) {
|
if (query.data?.user) {
|
||||||
setUser(query.data.user);
|
setUser(query.data.user);
|
||||||
}
|
}
|
||||||
}, [query.data, setUser]);
|
}, [query.data, setUser]);
|
||||||
|
|||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import { useRef, useState, useCallback, useEffect } from "react";
|
||||||
|
import { SwitchCamera, CameraOff, Aperture } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
interface CameraCaptureProps {
|
||||||
|
onCapture: (file: File) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FacingMode = "environment" | "user";
|
||||||
|
|
||||||
|
export function CameraCapture({ onCapture, onClose }: CameraCaptureProps) {
|
||||||
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
|
const streamRef = useRef<MediaStream | null>(null);
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
|
const [facingMode, setFacingMode] = useState<FacingMode>("environment");
|
||||||
|
const [status, setStatus] = useState<"loading" | "ready" | "error" | "denied">("loading");
|
||||||
|
const [errorMsg, setErrorMsg] = useState<string>("");
|
||||||
|
|
||||||
|
const stopStream = useCallback(() => {
|
||||||
|
if (streamRef.current) {
|
||||||
|
streamRef.current.getTracks().forEach((t) => t.stop());
|
||||||
|
streamRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const startCamera = useCallback(
|
||||||
|
async (mode: FacingMode) => {
|
||||||
|
stopStream();
|
||||||
|
setStatus("loading");
|
||||||
|
setErrorMsg("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: {
|
||||||
|
facingMode: mode,
|
||||||
|
width: { ideal: 1920 },
|
||||||
|
height: { ideal: 1080 },
|
||||||
|
},
|
||||||
|
audio: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
streamRef.current = stream;
|
||||||
|
if (videoRef.current) {
|
||||||
|
videoRef.current.srcObject = stream;
|
||||||
|
await videoRef.current.play();
|
||||||
|
}
|
||||||
|
setStatus("ready");
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const e = err as DOMException;
|
||||||
|
if (e.name === "NotAllowedError" || e.name === "PermissionDeniedError") {
|
||||||
|
setStatus("denied");
|
||||||
|
setErrorMsg("Izin kamera ditolak. Buka pengaturan untuk mengizinkan akses kamera.");
|
||||||
|
} else if (e.name === "NotFoundError") {
|
||||||
|
setStatus("error");
|
||||||
|
setErrorMsg("Kamera tidak ditemukan pada perangkat ini.");
|
||||||
|
} else if (e.name === "NotReadableError") {
|
||||||
|
setStatus("error");
|
||||||
|
setErrorMsg("Kamera sedang digunakan oleh aplikasi lain.");
|
||||||
|
} else {
|
||||||
|
setStatus("error");
|
||||||
|
setErrorMsg(`Gagal mengakses kamera: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[stopStream],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Start camera on mount
|
||||||
|
useEffect(() => {
|
||||||
|
startCamera(facingMode);
|
||||||
|
return () => stopStream();
|
||||||
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const toggleFacing = () => {
|
||||||
|
const next = facingMode === "environment" ? "user" : "environment";
|
||||||
|
setFacingMode(next);
|
||||||
|
startCamera(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCapture = () => {
|
||||||
|
const video = videoRef.current;
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
if (!video || !canvas) return;
|
||||||
|
|
||||||
|
const vw = video.videoWidth;
|
||||||
|
const vh = video.videoHeight;
|
||||||
|
canvas.width = vw;
|
||||||
|
canvas.height = vh;
|
||||||
|
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
ctx.drawImage(video, 0, 0, vw, vh);
|
||||||
|
canvas.toBlob(
|
||||||
|
(blob) => {
|
||||||
|
if (!blob) return;
|
||||||
|
const file = new File([blob], `camera-${Date.now()}.jpg`, {
|
||||||
|
type: "image/jpeg",
|
||||||
|
});
|
||||||
|
stopStream();
|
||||||
|
onCapture(file);
|
||||||
|
},
|
||||||
|
"image/jpeg",
|
||||||
|
0.92,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center gap-3 w-full">
|
||||||
|
{/* Viewfinder */}
|
||||||
|
<div className="relative w-full rounded-xl overflow-hidden bg-black aspect-[4/3] max-h-[420px]">
|
||||||
|
{status === "loading" && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-black/80 text-white">
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<div className="h-8 w-8 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
|
<span className="text-sm">Membuka kamera...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(status === "error" || status === "denied") && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center bg-black/90 text-white p-6">
|
||||||
|
<div className="flex flex-col items-center gap-3 text-center">
|
||||||
|
<CameraOff className="text-red-400" size={40} />
|
||||||
|
<p className="text-sm text-red-300">{errorMsg}</p>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="h-9 px-3 text-sm text-white border-white/30 hover:bg-white/10"
|
||||||
|
onClick={() => startCamera(facingMode)}
|
||||||
|
>
|
||||||
|
Coba Lagi
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
autoPlay
|
||||||
|
playsInline
|
||||||
|
muted
|
||||||
|
className={`w-full h-full object-cover ${status === "ready" ? "opacity-100" : "opacity-0"}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Scan area overlay */}
|
||||||
|
{status === "ready" && (
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||||
|
<div className="absolute inset-0 bg-black/20" />
|
||||||
|
<div
|
||||||
|
className="relative flex items-center justify-center"
|
||||||
|
style={{ width: "70%", height: "75%" }}
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 left-0 w-6 h-6 border-t-2 border-l-2 border-lime-300" />
|
||||||
|
<div className="absolute top-0 right-0 w-6 h-6 border-t-2 border-r-2 border-lime-300" />
|
||||||
|
<div className="absolute bottom-0 left-0 w-6 h-6 border-b-2 border-l-2 border-lime-300" />
|
||||||
|
<div className="absolute bottom-0 right-0 w-6 h-6 border-b-2 border-r-2 border-lime-300" />
|
||||||
|
<div className="text-white text-center flex flex-col gap-1">
|
||||||
|
<span className="text-xs font-semibold tracking-widest">
|
||||||
|
AREA SCAN
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="flex items-center justify-center gap-4 w-full">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="rounded-full h-12 w-12 p-0"
|
||||||
|
onClick={onClose}
|
||||||
|
title="Tutup kamera"
|
||||||
|
>
|
||||||
|
<CameraOff size={20} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="rounded-full h-16 w-16 p-0 bg-white border-4 border-green-500 hover:bg-green-50"
|
||||||
|
onClick={handleCapture}
|
||||||
|
disabled={status !== "ready"}
|
||||||
|
title="Ambil foto"
|
||||||
|
>
|
||||||
|
<Aperture className="text-green-600" size={32} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="rounded-full h-12 w-12 p-0"
|
||||||
|
onClick={toggleFacing}
|
||||||
|
title="Ganti kamera"
|
||||||
|
>
|
||||||
|
<SwitchCamera size={20} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hidden canvas for capture */}
|
||||||
|
<canvas ref={canvasRef} className="hidden" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,12 +9,13 @@ import {
|
|||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
FlaskConical,
|
FlaskConical,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
Camera,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { RiskBadge } from "@/components/risk-badge";
|
import { RiskBadge } from "@/components/risk-badge";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
confidence: number; // contoh: 0.95
|
confidence: number;
|
||||||
diseaseName: string;
|
diseaseName: string;
|
||||||
scientificName: string;
|
scientificName: string;
|
||||||
riskLevel: string;
|
riskLevel: string;
|
||||||
@@ -41,8 +42,8 @@ export function DiagnosisResultView({
|
|||||||
|
|
||||||
const getRiskLevelKey = (level: string): "low" | "medium" | "high" => {
|
const getRiskLevelKey = (level: string): "low" | "medium" | "high" => {
|
||||||
const normalized = level.toLowerCase();
|
const normalized = level.toLowerCase();
|
||||||
if (normalized.includes("rendah")) return "low";
|
if (normalized.includes("rendah") || normalized === "low") return "low";
|
||||||
if (normalized.includes("tinggi")) return "high";
|
if (normalized.includes("tinggi") || normalized === "high") return "high";
|
||||||
return "medium";
|
return "medium";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -62,10 +63,8 @@ export function DiagnosisResultView({
|
|||||||
alt="Daun yang dianalisis"
|
alt="Daun yang dianalisis"
|
||||||
className="w-full h-48 md:h-64 object-cover rounded-xl"
|
className="w-full h-48 md:h-64 object-cover rounded-xl"
|
||||||
/>
|
/>
|
||||||
<div className="mt-3 flex items-center gap-2 text-slate-500 text-sm px-1">
|
<div className="mt-3 flex items-center gap-2 text-slate-500 text-md px-1">
|
||||||
<span className="w-4 h-4 bg-slate-200 rounded flex items-center justify-center text-[10px]">
|
<Camera className="w-5 h-5 text-gray-500" />
|
||||||
📷
|
|
||||||
</span>
|
|
||||||
Gambar yang dianalisis
|
Gambar yang dianalisis
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,18 +85,35 @@ export function DiagnosisResultView({
|
|||||||
<div className="space-y-2 mb-4">
|
<div className="space-y-2 mb-4">
|
||||||
<div className="flex justify-between text-sm font-bold text-slate-700">
|
<div className="flex justify-between text-sm font-bold text-slate-700">
|
||||||
<span>Tingkat Keyakinan AI</span>
|
<span>Tingkat Keyakinan AI</span>
|
||||||
<span className="text-emerald-600">{confidencePercent}%</span>
|
<span
|
||||||
|
className={
|
||||||
|
confidencePercent >= 75
|
||||||
|
? "text-emerald-600"
|
||||||
|
: "text-amber-500"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{confidencePercent}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-slate-200 rounded-full h-2.5 overflow-hidden">
|
<div className="w-full bg-slate-200 rounded-full h-2.5 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="bg-emerald-500 h-2.5 rounded-full transition-all duration-1000"
|
className={`h-2.5 rounded-full transition-all duration-1000 ${
|
||||||
|
confidencePercent >= 75 ? "bg-emerald-500" : "bg-amber-500"
|
||||||
|
}`}
|
||||||
style={{ width: `${confidencePercent}%` }}
|
style={{ width: `${confidencePercent}%` }}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-[11px] text-emerald-600 flex items-center gap-1 font-medium">
|
{confidencePercent >= 75 ? (
|
||||||
<CheckCircle2 className="w-3 h-3" /> Di atas ambang batas minimum
|
<p className="text-[11px] text-emerald-600 flex items-center gap-1 font-medium">
|
||||||
(75%)
|
<CheckCircle2 className="w-3 h-3" /> Di atas ambang batas
|
||||||
</p>
|
minimum (75%)
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-[11px] text-amber-600 flex items-center gap-1 font-medium">
|
||||||
|
<AlertTriangle className="w-3 h-3" /> Di bawah ambang batas
|
||||||
|
minimum (75%)
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 pt-4 border-t border-amber-200/50">
|
<div className="flex items-center gap-3 pt-4 border-t border-amber-200/50">
|
||||||
|
|||||||
@@ -14,7 +14,29 @@ import type {
|
|||||||
} from '@zeavis/shared';
|
} from '@zeavis/shared';
|
||||||
import { recordApiCall } from './telemetry';
|
import { recordApiCall } from './telemetry';
|
||||||
|
|
||||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? '';
|
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'https://zeavisedu.asepharyana.my.id';
|
||||||
|
|
||||||
|
const AUTH_TOKEN_KEY = 'zeavis_auth_token';
|
||||||
|
|
||||||
|
function getAuthToken(): string | null {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(AUTH_TOKEN_KEY);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setAuthToken(token: string | null) {
|
||||||
|
try {
|
||||||
|
if (token) {
|
||||||
|
localStorage.setItem(AUTH_TOKEN_KEY, token);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(AUTH_TOKEN_KEY);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// localStorage may throw in private browsing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiError extends Error {
|
export interface ApiError extends Error {
|
||||||
status: number;
|
status: number;
|
||||||
@@ -24,10 +46,21 @@ export interface ApiError extends Error {
|
|||||||
async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
const url = `${apiBaseUrl}${endpoint}`;
|
const url = `${apiBaseUrl}${endpoint}`;
|
||||||
|
const token = getAuthToken();
|
||||||
|
const headers = new Headers(options?.headers);
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers.set('Authorization', `Bearer ${token}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.body && !options.method) {
|
||||||
|
// auto-set Content-Type for JSON bodies
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
...options,
|
...options,
|
||||||
headers: options?.headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
const duration = performance.now() - start;
|
const duration = performance.now() - start;
|
||||||
@@ -93,23 +126,29 @@ export const apiClient = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async register(payload: RegisterRequest): Promise<AuthResponse> {
|
async register(payload: RegisterRequest): Promise<AuthResponse> {
|
||||||
return fetchApi('/api/v1/auth/register', {
|
const result = await fetchApi<AuthResponse>('/api/v1/auth/register', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
if (result.token) setAuthToken(result.token);
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
async login(payload: AuthRequest): Promise<AuthResponse> {
|
async login(payload: AuthRequest): Promise<AuthResponse> {
|
||||||
return fetchApi('/api/v1/auth/login', {
|
const result = await fetchApi<AuthResponse>('/api/v1/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
if (result.token) setAuthToken(result.token);
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
async logout(): Promise<{ ok: boolean }> {
|
async logout(): Promise<{ ok: boolean }> {
|
||||||
return fetchApi('/api/v1/auth/logout', { method: 'POST' });
|
const result = await fetchApi<{ ok: boolean }>('/api/v1/auth/logout', { method: 'POST' });
|
||||||
|
setAuthToken(null);
|
||||||
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Disease catalog methods
|
// Disease catalog methods
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Lightweight Tauri environment detection and utilities.
|
||||||
|
* Uses raw __TAURI_INTERNALS__ IPC to avoid bundling/import issues on Android.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let _isTauri: boolean | null = null;
|
||||||
|
|
||||||
|
export function isTauri(): boolean {
|
||||||
|
if (_isTauri !== null) return _isTauri;
|
||||||
|
_isTauri =
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
'__TAURI_INTERNALS__' in window;
|
||||||
|
return _isTauri;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get the Tauri IPC invoke function directly from the global internals. */
|
||||||
|
function tauriInvoke(): (cmd: string, args?: Record<string, unknown>) => Promise<unknown> {
|
||||||
|
const T = (window as any).__TAURI_INTERNALS__;
|
||||||
|
if (!T?.invoke) throw new Error('Tauri IPC not available');
|
||||||
|
return T.invoke.bind(T);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openUrl(url: string): Promise<void> {
|
||||||
|
if (!isTauri()) {
|
||||||
|
// Not in Tauri — normal browser navigation
|
||||||
|
window.location.href = url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const invoke = tauriInvoke();
|
||||||
|
await invoke('plugin:opener|open_url', { url });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Tauri openUrl failed, trying fallback:', err);
|
||||||
|
// Fallback: navigate the WebView (Google will block, but best effort)
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processDeepLinkUrl(url: string): void {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
const target = u.pathname + u.search + u.hash;
|
||||||
|
if (target && target !== '/') {
|
||||||
|
window.location.href = target;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch { /* fall through */ }
|
||||||
|
|
||||||
|
// Fallback: handle both :// and :/ custom schemes
|
||||||
|
let match = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
|
||||||
|
if (!match) match = url.match(/^[^:]+:\/(\/.*)?$/);
|
||||||
|
if (match?.[1]) window.location.href = match[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setupDeepLinkHandler(): Promise<void> {
|
||||||
|
if (!isTauri()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const invoke = tauriInvoke();
|
||||||
|
|
||||||
|
// Cold-start: app just opened via intent:// or custom scheme
|
||||||
|
invoke('plugin:deep-link|get_current')
|
||||||
|
.then((urls: any) => {
|
||||||
|
if (urls?.[0]) processDeepLinkUrl(urls[0]);
|
||||||
|
})
|
||||||
|
.catch(() => { /* plugin may not be registered yet */ });
|
||||||
|
|
||||||
|
// Warm-start: listen for new URLs while app is running
|
||||||
|
const { listen } = await import('@tauri-apps/api/event');
|
||||||
|
listen('deep-link://new-url', (event: any) => {
|
||||||
|
const urls = event.payload as string[];
|
||||||
|
for (const url of urls) processDeepLinkUrl(url);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Tauri deep-link setup failed:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -54,12 +54,12 @@ export function CatalogPage() {
|
|||||||
<div className="space-y-6 max-w-5xl mx-auto pb-10">
|
<div className="space-y-6 max-w-5xl mx-auto pb-10">
|
||||||
{/* Page Title */}
|
{/* Page Title */}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl md:text-3xl font-extrabold text-[#214B11]">
|
<h1 className="text-2xl font-bold text-emerald-800">
|
||||||
Pustaka Penyakit
|
Pustaka Penyakit
|
||||||
</h1>
|
</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
|
Referensi lengkap penyakit dan kondisi daun jagung yang dapat
|
||||||
dideteksi oleh sistem AI ZeaVis Edu.
|
dideteksi oleh sistem AI ZeaVis Edu
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { useUiStore } from "@/store/ui-store";
|
import { useUiStore } from "@/store/ui-store";
|
||||||
import { apiClient } from "@/lib/api-client";
|
import { apiClient } from "@/lib/api-client";
|
||||||
import bg from "@/assets/images/dashboard-bg.webp";
|
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const { dashboardCompact } = useUiStore();
|
const { dashboardCompact } = useUiStore();
|
||||||
@@ -23,16 +22,17 @@ export function DashboardPage() {
|
|||||||
const summary = summaryQuery.data;
|
const summary = summaryQuery.data;
|
||||||
const diseases = diseasesQuery.data ?? [];
|
const diseases = diseasesQuery.data ?? [];
|
||||||
|
|
||||||
const diseasesQuick = useMemo(() =>
|
const diseasesQuick = useMemo(
|
||||||
diseases
|
() =>
|
||||||
.sort((a, b) => a.displayOrder - b.displayOrder)
|
diseases
|
||||||
.map((d) => ({
|
.sort((a, b) => a.displayOrder - b.displayOrder)
|
||||||
name: d.commonName,
|
.map((d) => ({
|
||||||
sci: d.label,
|
name: d.commonName,
|
||||||
color: d.accentColor,
|
sci: d.label,
|
||||||
slug: d.slug,
|
color: d.accentColor,
|
||||||
})),
|
slug: d.slug,
|
||||||
[diseases]
|
})),
|
||||||
|
[diseases],
|
||||||
);
|
);
|
||||||
|
|
||||||
const missionCards = [
|
const missionCards = [
|
||||||
@@ -74,7 +74,11 @@ export function DashboardPage() {
|
|||||||
{/* Hero header */}
|
{/* Hero header */}
|
||||||
<header
|
<header
|
||||||
className="relative overflow-hidden rounded-3xl bg-cover bg-center bg-no-repeat shadow-sm"
|
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="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">
|
<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">
|
<span className="inline-block rounded-full bg-[#1E8A2A]/80 px-4 py-2 text-xs font-semibold">
|
||||||
AI FOR SMART EDUCATION
|
AI FOR SMART EDUCATION
|
||||||
</span>
|
</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]">
|
<h2 className="text-2xl md:text-4xl font-extrabold tracking-tight text-[#9AD872]">
|
||||||
ZeaVis Edu
|
ZeaVis Edu
|
||||||
</h2>
|
</h2>
|
||||||
@@ -107,10 +113,7 @@ export function DashboardPage() {
|
|||||||
variant="outline"
|
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]"
|
className="px-4 md:px-6 py-3 md:py-6 text-sm md:text-lg font-semibold text-white hover:bg-[#1E8A2A]"
|
||||||
>
|
>
|
||||||
<Link
|
<Link to="/catalog" className="inline-flex items-center gap-2">
|
||||||
to="/catalog"
|
|
||||||
className="inline-flex items-center gap-2"
|
|
||||||
>
|
|
||||||
Pustaka Penyakit
|
Pustaka Penyakit
|
||||||
<ChevronRight className="h-5 w-6" />
|
<ChevronRight className="h-5 w-6" />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -169,7 +172,10 @@ export function DashboardPage() {
|
|||||||
<div className="text-3xl font-bold">
|
<div className="text-3xl font-bold">
|
||||||
{summary.imageClassificationCount}
|
{summary.imageClassificationCount}
|
||||||
</div>
|
</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
|
Lihat daftar
|
||||||
</Link>
|
</Link>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -186,7 +192,10 @@ export function DashboardPage() {
|
|||||||
<div className="text-3xl font-bold text-amber-600">
|
<div className="text-3xl font-bold text-amber-600">
|
||||||
{summary.needsReviewCount}
|
{summary.needsReviewCount}
|
||||||
</div>
|
</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
|
Lihat daftar
|
||||||
</Link>
|
</Link>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -200,10 +209,11 @@ export function DashboardPage() {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="h-full flex flex-col justify-start pt-2">
|
<CardContent className="h-full flex flex-col justify-start pt-2">
|
||||||
<div className="text-3xl font-bold text-red-600">
|
<div className="text-3xl font-bold text-red-600">—</div>
|
||||||
—
|
<Link
|
||||||
</div>
|
to="/diagnoses?status=failed"
|
||||||
<Link to="/diagnoses?status=failed" className="text-red-600 ml-auto hover:underline">
|
className="text-red-600 ml-auto hover:underline"
|
||||||
|
>
|
||||||
Lihat daftar
|
Lihat daftar
|
||||||
</Link>
|
</Link>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -220,7 +230,10 @@ export function DashboardPage() {
|
|||||||
<div className="text-3xl font-bold text-red-600">
|
<div className="text-3xl font-bold text-red-600">
|
||||||
{summary.riskDistribution.high}
|
{summary.riskDistribution.high}
|
||||||
</div>
|
</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
|
Lihat pustaka
|
||||||
</Link>
|
</Link>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -276,7 +289,8 @@ export function DashboardPage() {
|
|||||||
Penyakit yang Dapat Dideteksi
|
Penyakit yang Dapat Dideteksi
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[15px] font-normal text-muted-foreground">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
@@ -295,9 +309,7 @@ export function DashboardPage() {
|
|||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
{diseasesQuick.map((d) => (
|
{diseasesQuick.map((d) => (
|
||||||
<Link key={d.slug} to={`/catalog/${d.slug}`}>
|
<Link key={d.slug} to={`/catalog/${d.slug}`}>
|
||||||
<Card
|
<Card className="rounded-2xl bg-white p-4 shadow-sm h-full transition-transform hover:scale-[1.03] hover:shadow-md cursor-pointer">
|
||||||
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">
|
<CardContent className="h-full p-4 flex flex-col justify-between">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div
|
<div
|
||||||
@@ -324,7 +336,9 @@ export function DashboardPage() {
|
|||||||
{/* Scan quick access */}
|
{/* 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="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">
|
<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">
|
<p className="text-sm text-[#9AD872] font-normal mt-2">
|
||||||
Unggah foto daun jagung Anda dan dapatkan hasil analisis AI
|
Unggah foto daun jagung Anda dan dapatkan hasil analisis AI
|
||||||
dalam hitungan detik.
|
dalam hitungan detik.
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function DiagnosesPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div>
|
<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">
|
<p className="text-gray-500 mt-1 text-md">
|
||||||
Lihat hasil diagnosa dari scan yang telah dilakukan
|
Lihat hasil diagnosa dari scan yang telah dilakukan
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -71,11 +71,7 @@ export function DiagnosisDetailPage() {
|
|||||||
Hasil deteksi AI dan panduan edukasi
|
Hasil deteksi AI dan panduan edukasi
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button asChild variant="outline">
|
||||||
asChild
|
|
||||||
variant="outline"
|
|
||||||
className="border-[#214B11] text-[#214B11] hover:bg-[#214B11] hover:text-white"
|
|
||||||
>
|
|
||||||
<Link to="/diagnoses">Kembali ke Riwayat</Link>
|
<Link to="/diagnoses">Kembali ke Riwayat</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export function ExpertReviewsPage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
|
||||||
<div>
|
<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">
|
<p className="text-gray-500 mt-1 text-md">
|
||||||
Tinjau hasil diagnosa dari scan yang telah dilakukan dan berikan
|
Tinjau hasil diagnosa dari scan yang telah dilakukan dan berikan
|
||||||
feedback untuk meningkatkan akurasi sistem AI ZeaVis Edu
|
feedback untuk meningkatkan akurasi sistem AI ZeaVis Edu
|
||||||
|
|||||||
@@ -1,15 +1,48 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { AuthForm } from "@/components/auth-form";
|
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";
|
import { useAuthStore } from "@/store/auth-store";
|
||||||
|
|
||||||
|
function getUrlParam(name: string): string | null {
|
||||||
|
return new URLSearchParams(window.location.search).get(name);
|
||||||
|
}
|
||||||
|
|
||||||
export function LoginPage() {
|
export function LoginPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const setUser = useAuthStore((state) => state.setUser);
|
const setUser = useAuthStore((state) => state.setUser);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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({
|
const meQuery = useQuery({
|
||||||
queryKey: ["auth", "me"],
|
queryKey: ["auth", "me"],
|
||||||
queryFn: () => apiClient.getMe(),
|
queryFn: () => apiClient.getMe(),
|
||||||
@@ -26,13 +59,39 @@ export function LoginPage() {
|
|||||||
setError(err instanceof Error ? err.message : "Login gagal"),
|
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 (
|
return (
|
||||||
<main className="flex min-h-screen items-center justify-center px-6 py-12">
|
<main className="relative 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
|
<AuthForm
|
||||||
mode="login"
|
mode="login"
|
||||||
isSubmitting={mutation.isPending}
|
isSubmitting={mutation.isPending}
|
||||||
error={error}
|
error={oauthError || error}
|
||||||
googleOAuthEnabled={Boolean(
|
googleOAuthEnabled={Boolean(
|
||||||
meQuery.data?.features.googleOAuthEnabled,
|
meQuery.data?.features.googleOAuthEnabled,
|
||||||
)}
|
)}
|
||||||
@@ -42,9 +101,13 @@ export function LoginPage() {
|
|||||||
}}
|
}}
|
||||||
onFieldChange={() => setError(null)}
|
onFieldChange={() => setError(null)}
|
||||||
/>
|
/>
|
||||||
<p className="text-center text-sm text-muted-foreground">
|
|
||||||
|
<p className="text-center text-sm text-slate-700">
|
||||||
Belum punya akun?{" "}
|
Belum punya akun?{" "}
|
||||||
<Link className="text-primary" to="/register">
|
<Link
|
||||||
|
className="text-emerald-700 font-bold hover:underline"
|
||||||
|
to="/register"
|
||||||
|
>
|
||||||
Daftar
|
Daftar
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -28,7 +28,18 @@ export function RegisterPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="flex min-h-screen items-center justify-center px-6 py-12">
|
<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
|
<AuthForm
|
||||||
mode="register"
|
mode="register"
|
||||||
isSubmitting={mutation.isPending}
|
isSubmitting={mutation.isPending}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useRef, useState } from "react";
|
import { useRef, useState, useCallback } from "react";
|
||||||
import { useNavigate, Link } from "react-router-dom";
|
import { useNavigate, Link } from "react-router-dom";
|
||||||
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQueryClient, useQuery } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
@@ -16,9 +16,11 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Modal } from "@/components/ui/modal";
|
import { Modal } from "@/components/ui/modal";
|
||||||
import type { DiagnosisRecord } from "@zeavis/shared";
|
import type { DiagnosisRecord } from "@zeavis/shared";
|
||||||
|
import { diseaseCatalogSeed } from "@zeavis/shared"; // Import data seed lokal ditambahkan
|
||||||
import { apiClient } from "@/lib/api-client";
|
import { apiClient } from "@/lib/api-client";
|
||||||
import { trackScan, trackDiagnosisResult } from "@/lib/telemetry";
|
import { trackScan, trackDiagnosisResult } from "@/lib/telemetry";
|
||||||
import { DiagnosisResultView } from "../components/diagnose-result-view";
|
import { DiagnosisResultView } from "../components/diagnose-result-view";
|
||||||
|
import { CameraCapture } from "../components/camera-capture";
|
||||||
|
|
||||||
export function ScanPage() {
|
export function ScanPage() {
|
||||||
const [fileName, setFileName] = useState<string | null>(null);
|
const [fileName, setFileName] = useState<string | null>(null);
|
||||||
@@ -32,6 +34,21 @@ export function ScanPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// Camera mode state
|
||||||
|
const [useCamera, setUseCamera] = useState(false);
|
||||||
|
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 mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: (file: File) => apiClient.createDiagnosis(file),
|
mutationFn: (file: File) => apiClient.createDiagnosis(file),
|
||||||
onSuccess: (diagnosis) => {
|
onSuccess: (diagnosis) => {
|
||||||
@@ -76,7 +93,7 @@ export function ScanPage() {
|
|||||||
const [diagnosisPreview, setDiagnosisPreview] =
|
const [diagnosisPreview, setDiagnosisPreview] =
|
||||||
useState<DiagnosisRecord | null>(null);
|
useState<DiagnosisRecord | null>(null);
|
||||||
|
|
||||||
const diagnosesQuery = useQuery({
|
useQuery({
|
||||||
queryKey: ["diagnoses"],
|
queryKey: ["diagnoses"],
|
||||||
queryFn: () => apiClient.getDiagnoses(),
|
queryFn: () => apiClient.getDiagnoses(),
|
||||||
enabled: previewOpen,
|
enabled: previewOpen,
|
||||||
@@ -87,10 +104,10 @@ export function ScanPage() {
|
|||||||
{/* Main Header */}
|
{/* Main Header */}
|
||||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4">
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4">
|
||||||
<div>
|
<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">
|
<p className="text-gray-500 mt-1 text-md">
|
||||||
Unggah foto daun jagung untuk dianalisis oleh sistem AI kami secara
|
Unggah foto daun jagung untuk dianalisis oleh sistem AI kami secara
|
||||||
real-time.
|
real-time
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
@@ -111,33 +128,74 @@ export function ScanPage() {
|
|||||||
|
|
||||||
{!previewUrl ? (
|
{!previewUrl ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div
|
{/* Mode toggle */}
|
||||||
className="w-full border-2 border-dashed border-green-300 rounded-md p-10 h-60 text-center cursor-pointer"
|
<div className="flex rounded-lg bg-gray-100 p-1">
|
||||||
onClick={() => inputRef.current?.click()}
|
<button
|
||||||
>
|
type="button"
|
||||||
<Upload className="mx-auto text-green-500 mb-3" size={48} />
|
onClick={() => setUseCamera(false)}
|
||||||
<h3 className="font-semibold text-base text-gray-800 mb-1">
|
className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-colors ${
|
||||||
Seret & Lepas Foto Daun
|
!useCamera
|
||||||
</h3>
|
? "bg-white text-green-700 shadow-sm"
|
||||||
<p className="text-xs text-gray-500 mb-3">
|
: "text-gray-500 hover:text-gray-700"
|
||||||
atau klik untuk memilih file berkas dari perangkat Anda
|
}`}
|
||||||
</p>
|
>
|
||||||
<div className="flex flex-wrap gap-2 justify-center">
|
<Upload size={16} className="inline mr-1.5" />
|
||||||
<div className="bg-green-100 text-green-700 px-3 py-1 rounded-full inline-flex items-center gap-1 text-xs font-medium">
|
Unggah
|
||||||
<Check size={14} /> PNG, JPG, JPEG, WEBP
|
</button>
|
||||||
</div>
|
<button
|
||||||
<div className="bg-green-100 text-green-700 px-3 py-1 rounded-full inline-flex items-center gap-1 text-xs font-medium">
|
type="button"
|
||||||
<Check size={14} /> Maks. 5 MB
|
onClick={() => setUseCamera(true)}
|
||||||
</div>
|
className={`flex-1 py-2 px-3 rounded-md text-sm font-medium transition-colors ${
|
||||||
</div>
|
useCamera
|
||||||
|
? "bg-white text-green-700 shadow-sm"
|
||||||
|
: "text-gray-500 hover:text-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Camera size={16} className="inline mr-1.5" />
|
||||||
|
Kamera
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
type="button"
|
{useCamera ? (
|
||||||
onClick={() => inputRef.current?.click()}
|
<CameraCapture
|
||||||
className="w-full bg-green-600 hover:bg-green-700 text-white font-medium py-2.5 px-4 rounded-lg transition-colors flex items-center justify-center gap-2"
|
onCapture={handleCameraCapture}
|
||||||
>
|
onClose={() => setUseCamera(false)}
|
||||||
<Upload size={18} /> Pilih Berkas
|
/>
|
||||||
</button>
|
) : (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
<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
|
||||||
|
</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">
|
||||||
|
<Check size={14} /> PNG, JPG, JPEG, WEBP
|
||||||
|
</div>
|
||||||
|
<div className="bg-green-100 text-green-700 px-3 py-1 rounded-full inline-flex items-center gap-1 text-xs font-medium">
|
||||||
|
<Check size={14} /> Maks. 5 MB
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => inputRef.current?.click()}
|
||||||
|
className="w-full bg-green-600 hover:bg-green-700 text-white font-medium py-2.5 px-4 rounded-lg transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<Upload size={18} /> Pilih Berkas
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -373,29 +431,46 @@ export function ScanPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DiagnosisResultView
|
{/* Render DiagnosisResultView dengan Fallback Obat */}
|
||||||
imageUrl={
|
{(() => {
|
||||||
previewUrl ||
|
// Fallback logic for scientific name and medicine recommendations
|
||||||
diagnosisPreview.imageUrl ||
|
const seedData = diagnosisPreview.disease
|
||||||
"https://placehold.co/600x400?text=Foto+Daun"
|
? diseaseCatalogSeed.find(
|
||||||
}
|
(seed) =>
|
||||||
confidence={diagnosisPreview.confidence ?? 0}
|
seed.commonName === diagnosisPreview.disease?.commonName,
|
||||||
diseaseName={
|
)
|
||||||
diagnosisPreview.disease?.commonName ?? "Tidak Diketahui"
|
: null;
|
||||||
}
|
|
||||||
scientificName={diagnosisPreview.disease?.label ?? ""}
|
// If the API doesn't return medicine recommendations, use the seed data as a fallback
|
||||||
riskLevel={diagnosisPreview.disease?.riskLevel ?? "Sedang"}
|
const finalMedicines =
|
||||||
description={
|
(diagnosisPreview.disease as any)?.medicineRecommendations ||
|
||||||
diagnosisPreview.disease?.description ??
|
seedData?.medicineRecommendations ||
|
||||||
diagnosisPreview.disease?.summary ??
|
[];
|
||||||
"Deskripsi tidak tersedia."
|
|
||||||
}
|
return (
|
||||||
symptoms={diagnosisPreview.disease?.symptoms ?? []}
|
<DiagnosisResultView
|
||||||
preventions={diagnosisPreview.disease?.recommendations ?? []}
|
imageUrl={
|
||||||
medicines={
|
previewUrl ||
|
||||||
(diagnosisPreview.disease as any)?.medicineRecommendations ?? []
|
diagnosisPreview.imageUrl ||
|
||||||
}
|
"https://placehold.co/600x400?text=Foto+Daun"
|
||||||
/>
|
}
|
||||||
|
confidence={diagnosisPreview.confidence ?? 0}
|
||||||
|
diseaseName={
|
||||||
|
diagnosisPreview.disease?.commonName ?? "Tidak Diketahui"
|
||||||
|
}
|
||||||
|
scientificName={diagnosisPreview.disease?.label ?? ""}
|
||||||
|
riskLevel={diagnosisPreview.disease?.riskLevel ?? "Sedang"}
|
||||||
|
description={
|
||||||
|
diagnosisPreview.disease?.description ??
|
||||||
|
diagnosisPreview.disease?.summary ??
|
||||||
|
"Deskripsi tidak tersedia."
|
||||||
|
}
|
||||||
|
symptoms={diagnosisPreview.disease?.symptoms ?? []}
|
||||||
|
preventions={diagnosisPreview.disease?.recommendations ?? []}
|
||||||
|
medicines={finalMedicines} // Datanya terhubung ke sini!
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* All Model Predictions */}
|
{/* All Model Predictions */}
|
||||||
{diagnosisPreview.predictions &&
|
{diagnosisPreview.predictions &&
|
||||||
|
|||||||
@@ -286,12 +286,11 @@ export function TelemetryPage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl md:text-[28px] font-extrabold text-[#214B11] flex items-center gap-3">
|
<h1 className="text-2xl font-bold text-emerald-800">
|
||||||
<Activity className="h-7 w-7 text-[#48A111]" />
|
|
||||||
Telemetry Dashboard
|
Telemetry Dashboard
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-muted-foreground mt-0.5">
|
<p className="text-gray-500 mt-1 text-md">
|
||||||
Real-time metrics from Prometheus
|
Real-time monitoring dari performa sistem dan aplikasi ZeaVis Edu
|
||||||
{error && <span className="text-amber-600 ml-2">(partial — {error})</span>}
|
{error && <span className="text-amber-600 ml-2">(partial — {error})</span>}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+16
-1
@@ -9,7 +9,22 @@ export default defineConfig(({ mode }) => {
|
|||||||
const apiProxyTarget = env.VITE_API_PROXY_TARGET || 'http://localhost:3000';
|
const apiProxyTarget = env.VITE_API_PROXY_TARGET || 'http://localhost:3000';
|
||||||
|
|
||||||
return {
|
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: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': apiProxyTarget,
|
'/api': apiProxyTarget,
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"configVersion": 0,
|
"configVersion": 1,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
|
"name": "zeavis-edu",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "2.11.0",
|
||||||
|
"@tauri-apps/plugin-deep-link": "2.4.9",
|
||||||
|
"@tauri-apps/plugin-opener": "2.5.4",
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@moonrepo/cli": "^2.2.5",
|
"@moonrepo/cli": "^2.2.5",
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
@@ -35,6 +41,9 @@
|
|||||||
"apps/tauri": {
|
"apps/tauri": {
|
||||||
"name": "@zeavis/tauri",
|
"name": "@zeavis/tauri",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"sharp": "0.35.1",
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2",
|
"@tauri-apps/cli": "^2",
|
||||||
},
|
},
|
||||||
@@ -45,6 +54,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"@tanstack/react-query": "^5.100.11",
|
"@tanstack/react-query": "^5.100.11",
|
||||||
|
"@tauri-apps/plugin-deep-link": "^2",
|
||||||
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"@zeavis/shared": "workspace:*",
|
"@zeavis/shared": "workspace:*",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -55,6 +66,7 @@
|
|||||||
"react-router-dom": "^7.15.1",
|
"react-router-dom": "^7.15.1",
|
||||||
"recharts": "3.8.1",
|
"recharts": "3.8.1",
|
||||||
"tailwind-merge": "^3.6.0",
|
"tailwind-merge": "^3.6.0",
|
||||||
|
"tailwindcss": "^4.3.1",
|
||||||
"web-vitals": "5.3.0",
|
"web-vitals": "5.3.0",
|
||||||
"zustand": "^5.0.13",
|
"zustand": "^5.0.13",
|
||||||
},
|
},
|
||||||
@@ -87,7 +99,7 @@
|
|||||||
|
|
||||||
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
|
"@emnapi/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=="],
|
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||||
|
|
||||||
@@ -151,6 +163,60 @@
|
|||||||
|
|
||||||
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="],
|
"@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/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=="],
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
@@ -163,23 +229,23 @@
|
|||||||
|
|
||||||
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
|
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
|
||||||
|
|
||||||
"@moonrepo/cli": ["@moonrepo/cli@2.2.5", "", { "dependencies": { "detect-libc": "^2.1.2" }, "optionalDependencies": { "@moonrepo/core-linux-arm64-gnu": "2.2.5", "@moonrepo/core-linux-arm64-musl": "2.2.5", "@moonrepo/core-linux-x64-gnu": "2.2.5", "@moonrepo/core-linux-x64-musl": "2.2.5", "@moonrepo/core-macos-arm64": "2.2.5", "@moonrepo/core-macos-x64": "2.2.5", "@moonrepo/core-windows-x64-msvc": "2.2.5" }, "bin": { "moon": "moon.js", "moonx": "moonx.js" } }, "sha512-MZPXJ7pvfcC77jedFPxwW4Z4XCPzeuBMmbVjRDFXq13bnyAYcaH5kdxkvyxX4WHeEHWyWuustOaJowZo3Q/SxA=="],
|
"@moonrepo/cli": ["@moonrepo/cli@2.3.3", "", { "dependencies": { "detect-libc": "^2.1.2" }, "optionalDependencies": { "@moonrepo/core-linux-arm64-gnu": "2.3.3", "@moonrepo/core-linux-arm64-musl": "2.3.3", "@moonrepo/core-linux-x64-gnu": "2.3.3", "@moonrepo/core-linux-x64-musl": "2.3.3", "@moonrepo/core-macos-arm64": "2.3.3", "@moonrepo/core-macos-x64": "2.3.3", "@moonrepo/core-windows-x64-msvc": "2.3.3" }, "bin": { "moon": "moon.js", "moonx": "moonx.js" } }, "sha512-TmHbQFJH1+vqIsXn7WpPXQtPlgwgyfWUjUBWWQCR2dj3UHg/0IGD4KDlqNg1VPmcyXnuDmlcHtqZ2bkxZx+RhQ=="],
|
||||||
|
|
||||||
"@moonrepo/core-linux-arm64-gnu": ["@moonrepo/core-linux-arm64-gnu@2.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-61ajwr3oxCAC0vp879LgKzml3pu+kJ0xMd2Tr1fFMuqp+Xp9k+41VOaTlW7m3vhNQCTVuFFGKxh8KUeAtJk//w=="],
|
"@moonrepo/core-linux-arm64-gnu": ["@moonrepo/core-linux-arm64-gnu@2.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-AzovoQPizmwGe5nLd9s0UnN/IUnol81la2vNH+U7vSyh24NzVirT1iOLEcajwtbNvJpl3aXzbhbjrXp35fKVWg=="],
|
||||||
|
|
||||||
"@moonrepo/core-linux-arm64-musl": ["@moonrepo/core-linux-arm64-musl@2.2.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-8gcRf1B+jxbdy4rdQzfXSg37ECsiThCKYHYYXfe4etzcC6CQjmmYeC7OxF+gxWK0mL6UbpuKso8zOcqSbYzjYw=="],
|
"@moonrepo/core-linux-arm64-musl": ["@moonrepo/core-linux-arm64-musl@2.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-2jJLXAIUiqdFdw3hxuMefGB3DenQN/CC2mbqL44jC4uZNH6pX1mvjlguubBwUvGHfGbACjh9W9JjavpSMKY9cw=="],
|
||||||
|
|
||||||
"@moonrepo/core-linux-x64-gnu": ["@moonrepo/core-linux-x64-gnu@2.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-kqZppmKKzdStXbOpWUS+rQlJiowO0ItwWP74oqJVpwUewAjqB2WLwG6IvhSAVHrVhC+9OCGm0g4iAESQBAEh2w=="],
|
"@moonrepo/core-linux-x64-gnu": ["@moonrepo/core-linux-x64-gnu@2.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-17WZG59QUJhw8wepo1Oe5lmh/q0ax0EQzVk7/EbRe2uBRFsN24oR+R6MyVvuEqmG71x5EFwfPbuH/R48dcRROQ=="],
|
||||||
|
|
||||||
"@moonrepo/core-linux-x64-musl": ["@moonrepo/core-linux-x64-musl@2.2.5", "", { "os": "linux", "cpu": "x64" }, "sha512-UxyYR+SJ7er7Skb60saphEWsPN+oecqDIrNxdAYy+3Li7G9jzBK0j59Jj7j2c0pjlBSKao06DGmfM6WuBlE8eg=="],
|
"@moonrepo/core-linux-x64-musl": ["@moonrepo/core-linux-x64-musl@2.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-IaE5qN/QOi85SMdhpTtwrDVmIu1MHKznQZxB+jl8dUbQj5iWKyY/DusXXNs9zRR45jBNUlyE4FhrY9rwf2SqtQ=="],
|
||||||
|
|
||||||
"@moonrepo/core-macos-arm64": ["@moonrepo/core-macos-arm64@2.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DpvFXa/N4rc/vJygE6JBL1pW0DtX00h07xruWQYjdeOuv3RQClRQSwZJNJmGZKhZus4g/LccHgEGvRdu9IMC4w=="],
|
"@moonrepo/core-macos-arm64": ["@moonrepo/core-macos-arm64@2.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UiO8QuICnxD1XglNDCjkTBwL0YOHvfcj37m5yeIrBjyvkytlWNek4Us7jqr15gbYT+z9pU02Um8k+S1p7y+BkA=="],
|
||||||
|
|
||||||
"@moonrepo/core-macos-x64": ["@moonrepo/core-macos-x64@2.2.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-J4udAbRTnK+EarHu7nIKm2j5t4CY0I9hfoyGOSU9bsC3n91//YNrixPnCe34IA4irArNzqPnoDXlQv/v5mG8Pg=="],
|
"@moonrepo/core-macos-x64": ["@moonrepo/core-macos-x64@2.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-FteLklMxJ4cUuZnhrt5lFsYnQMqdzMiAaTFOQDVpUkd6llgrwxIJbUMnPOFHZe4jVJCjpmacEsG7j9/Oxg7HqQ=="],
|
||||||
|
|
||||||
"@moonrepo/core-windows-x64-msvc": ["@moonrepo/core-windows-x64-msvc@2.2.5", "", { "os": "win32", "cpu": "x64" }, "sha512-yYPMsOHL5daEPSTMp2JP+YrwmGjeu+Kp25UmRnllGDmofkCu4Gcv3ct6ljK+u1+W6q2Jc2ooY7eZ14dnKJnlug=="],
|
"@moonrepo/core-windows-x64-msvc": ["@moonrepo/core-windows-x64-msvc@2.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-qxXvgnqkE4yu6m0vcgDNmRBLnxfAVXq7F7ebaHAOmj7cK0DNnQpZBaxNB3LrIuxhyFdviKVTsUT7F7CpLsVjlw=="],
|
||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="],
|
||||||
|
|
||||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
"@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="],
|
||||||
|
|
||||||
@@ -241,7 +307,7 @@
|
|||||||
|
|
||||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
|
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.41.1", "", {}, "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA=="],
|
||||||
|
|
||||||
"@oxc-project/types": ["@oxc-project/types@0.132.0", "", {}, "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ=="],
|
"@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="],
|
||||||
|
|
||||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||||
|
|
||||||
@@ -255,49 +321,47 @@
|
|||||||
|
|
||||||
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||||
|
|
||||||
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.2", "", {}, "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw=="],
|
|
||||||
|
|
||||||
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||||
|
|
||||||
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||||
|
|
||||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="],
|
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.1", "", {}, "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="],
|
||||||
|
|
||||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA=="],
|
||||||
|
|
||||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-rCMO3QsIVKv5JTY5CVbo2MvO77SpEqqYc8AvRE7OWqRDOIqAKjsp+DrmnY9uc8NPdxB5E2z47HTYGeE2+NTptg=="],
|
||||||
|
|
||||||
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.12.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw=="],
|
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.12.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw=="],
|
||||||
|
|
||||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.2", "", { "os": "android", "cpu": "arm64" }, "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ=="],
|
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.3", "", { "os": "android", "cpu": "arm64" }, "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw=="],
|
||||||
|
|
||||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w=="],
|
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA=="],
|
||||||
|
|
||||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA=="],
|
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg=="],
|
||||||
|
|
||||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA=="],
|
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.2", "", { "os": "linux", "cpu": "arm" }, "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w=="],
|
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig=="],
|
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw=="],
|
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA=="],
|
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ=="],
|
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ=="],
|
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg=="],
|
||||||
|
|
||||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.2", "", { "os": "linux", "cpu": "x64" }, "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw=="],
|
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow=="],
|
||||||
|
|
||||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.2", "", { "os": "none", "cpu": "arm64" }, "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w=="],
|
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg=="],
|
||||||
|
|
||||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.2", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ=="],
|
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.3", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg=="],
|
||||||
|
|
||||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A=="],
|
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g=="],
|
||||||
|
|
||||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.2", "", { "os": "win32", "cpu": "x64" }, "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ=="],
|
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||||
|
|
||||||
@@ -307,39 +371,41 @@
|
|||||||
|
|
||||||
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
"@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
|
||||||
|
|
||||||
"@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g=="],
|
"@tailwindcss/node": ["@tailwindcss/node@4.3.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "5.21.6", "jiti": "^2.7.0", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.1" } }, "sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.0", "@tailwindcss/oxide-darwin-arm64": "4.3.0", "@tailwindcss/oxide-darwin-x64": "4.3.0", "@tailwindcss/oxide-freebsd-x64": "4.3.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", "@tailwindcss/oxide-linux-x64-musl": "4.3.0", "@tailwindcss/oxide-wasm32-wasi": "4.3.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg=="],
|
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.3.1", "@tailwindcss/oxide-darwin-arm64": "4.3.1", "@tailwindcss/oxide-darwin-x64": "4.3.1", "@tailwindcss/oxide-freebsd-x64": "4.3.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.3.1", "@tailwindcss/oxide-linux-arm64-musl": "4.3.1", "@tailwindcss/oxide-linux-x64-gnu": "4.3.1", "@tailwindcss/oxide-linux-x64-musl": "4.3.1", "@tailwindcss/oxide-wasm32-wasi": "4.3.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.3.1", "@tailwindcss/oxide-win32-x64-msvc": "4.3.1" } }, "sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.0", "", { "os": "android", "cpu": "arm64" }, "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng=="],
|
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.3.1", "", { "os": "android", "cpu": "arm64" }, "sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ=="],
|
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA=="],
|
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.3.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ=="],
|
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.3.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA=="],
|
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1", "", { "os": "linux", "cpu": "arm" }, "sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg=="],
|
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ=="],
|
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.3.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ=="],
|
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg=="],
|
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.3.1", "", { "os": "linux", "cpu": "x64" }, "sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.0", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA=="],
|
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.3.1", "", { "dependencies": { "@emnapi/core": "^1.10.0", "@emnapi/runtime": "^1.10.0", "@emnapi/wasi-threads": "^1.2.1", "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.2", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ=="],
|
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.3.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA=="],
|
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.3.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA=="],
|
||||||
|
|
||||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w=="],
|
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.1", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.1", "@tailwindcss/oxide": "4.3.1", "postcss": "8.5.15", "tailwindcss": "4.3.1" } }, "sha512-dNJuNbdEJT/SWRuXTYP1WSamelsz3ztkUsdtWQPjrexysrTpaEPM40P/71knXiXLYEojqPOEGitVLLpPMS5T6A=="],
|
||||||
|
|
||||||
"@tanstack/query-core": ["@tanstack/query-core@5.100.11", "", {}, "sha512-lmE0994apShXPj8CUxgx4ch5yUJhE9k/+tVwihBvPOyerACWdBocfFg24t8+0RhtlTd7tEgchDkhlCxNssvDxw=="],
|
"@tanstack/query-core": ["@tanstack/query-core@5.101.0", "", {}, "sha512-cQetA74EB+seWySv1TTKr828TnP0u39m6LykwDXIo84SNortpDkp30TMEjkqtYCNP9c40uT/iwl6MLiufEt0Ow=="],
|
||||||
|
|
||||||
"@tanstack/react-query": ["@tanstack/react-query@5.100.11", "", { "dependencies": { "@tanstack/query-core": "5.100.11" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-J0f9s5x3LE1450nNNfYx+e/n0DMa0uOBdFJUy5r0RvmsXd4nB/n0rbHtHI1vYXhikNFan+wf51p6Tmp4c8ucrg=="],
|
"@tanstack/react-query": ["@tanstack/react-query@5.101.0", "", { "dependencies": { "@tanstack/query-core": "5.101.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-rLlJXSpkqfizLWgkR5+eLeIk0MvTx/meEIR7LRjxic+qxiQP8zVjq7BqQkiCMNLQBlLfuOLqqr6KO5GtrDlmSg=="],
|
||||||
|
|
||||||
|
"@tauri-apps/api": ["@tauri-apps/api@2.11.0", "", {}, "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA=="],
|
||||||
|
|
||||||
"@tauri-apps/cli": ["@tauri-apps/cli@2.11.2", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.2", "@tauri-apps/cli-darwin-x64": "2.11.2", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", "@tauri-apps/cli-linux-arm64-musl": "2.11.2", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-musl": "2.11.2", "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", "@tauri-apps/cli-win32-x64-msvc": "2.11.2" }, "bin": { "tauri": "tauri.js" } }, "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw=="],
|
"@tauri-apps/cli": ["@tauri-apps/cli@2.11.2", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.11.2", "@tauri-apps/cli-darwin-x64": "2.11.2", "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", "@tauri-apps/cli-linux-arm64-musl": "2.11.2", "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-gnu": "2.11.2", "@tauri-apps/cli-linux-x64-musl": "2.11.2", "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", "@tauri-apps/cli-win32-x64-msvc": "2.11.2" }, "bin": { "tauri": "tauri.js" } }, "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw=="],
|
||||||
|
|
||||||
@@ -365,6 +431,10 @@
|
|||||||
|
|
||||||
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.2", "", { "os": "win32", "cpu": "x64" }, "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA=="],
|
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.11.2", "", { "os": "win32", "cpu": "x64" }, "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA=="],
|
||||||
|
|
||||||
|
"@tauri-apps/plugin-deep-link": ["@tauri-apps/plugin-deep-link@2.4.9", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-u0SKOUHnJ1wqeqXsDFq2+kASCBj9xxbG0g9XZWPy9SOmU4wXtp6b/wiYpm6oH6/5fBTQsLqnLhIvqLBRpgHJlA=="],
|
||||||
|
|
||||||
|
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.4", "", { "dependencies": { "@tauri-apps/api": "^2.11.0" } }, "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ=="],
|
||||||
|
|
||||||
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
|
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
|
||||||
|
|
||||||
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
|
||||||
@@ -393,9 +463,9 @@
|
|||||||
|
|
||||||
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
"@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.9.1", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
|
"@types/node": ["@types/node@25.9.3", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-603BddQMv3pUcr4U2dhujk83N2tTDVr/34wII2B6bJy6g+8WD6yUb11jszNs0gdi4PesVWl7ABt8nYMVpnLUcg=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q=="],
|
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
|
||||||
|
|
||||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
|
||||||
|
|
||||||
@@ -411,7 +481,7 @@
|
|||||||
|
|
||||||
"@zeavis/web": ["@zeavis/web@workspace:apps/web"],
|
"@zeavis/web": ["@zeavis/web@workspace:apps/web"],
|
||||||
|
|
||||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
"acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="],
|
||||||
|
|
||||||
"acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
|
"acorn-import-attributes": ["acorn-import-attributes@1.9.5", "", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
|
||||||
|
|
||||||
@@ -473,15 +543,15 @@
|
|||||||
|
|
||||||
"drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="],
|
"drizzle-kit": ["drizzle-kit@0.31.10", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw=="],
|
||||||
|
|
||||||
"drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="],
|
"drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="],
|
||||||
|
|
||||||
"elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="],
|
"elysia": ["elysia@1.4.28", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Vrx8sBnvq8squS/3yNBzR1jBXI+SgmnmvwawPjNuEHndUe5l1jV2Gp6JJ4ulDkEB8On6bWmmuyPpA+bq4t+WYg=="],
|
||||||
|
|
||||||
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||||
|
|
||||||
"enhanced-resolve": ["enhanced-resolve@5.22.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A=="],
|
"enhanced-resolve": ["enhanced-resolve@5.21.6", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ=="],
|
||||||
|
|
||||||
"es-toolkit": ["es-toolkit@1.47.0", "", {}, "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw=="],
|
"es-toolkit": ["es-toolkit@1.47.1", "", {}, "sha512-5RAqEwf4P4E17p+W75KLOWw/nOvKZzSQpxM32IpI2KZLaVonjTrZ0Ai5ghMaVI9eKC2p8eoQgcBdkEDgzFk6+Q=="],
|
||||||
|
|
||||||
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
"esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||||
|
|
||||||
@@ -513,7 +583,7 @@
|
|||||||
|
|
||||||
"immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="],
|
"immer": ["immer@10.2.0", "", {}, "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw=="],
|
||||||
|
|
||||||
"import-in-the-middle": ["import-in-the-middle@3.0.1", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA=="],
|
"import-in-the-middle": ["import-in-the-middle@3.0.2", "", { "dependencies": { "acorn": "^8.15.0", "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" } }, "sha512-LGLYRl0A2gtyUJb2WDliBHmk6TtlHwdDjxonacZ8QrEs/ZW+YDgNv2QAfjRQWpS8HqvNcq6GGnN6jrOa5FysDQ=="],
|
||||||
|
|
||||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||||
|
|
||||||
@@ -549,7 +619,7 @@
|
|||||||
|
|
||||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||||
|
|
||||||
"lucide-react": ["lucide-react@1.16.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ=="],
|
"lucide-react": ["lucide-react@1.18.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-LZDb7H/0YfM+RJncD0hDQRCAu+vSGODqpe35TuVI8EuXaRjkczbsx7p8dY4J87F/MUSj6bpYqeI8nw8qXaAdmA=="],
|
||||||
|
|
||||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||||
|
|
||||||
@@ -573,19 +643,19 @@
|
|||||||
|
|
||||||
"prom-client": ["prom-client@15.1.3", "", { "dependencies": { "@opentelemetry/api": "^1.4.0", "tdigest": "^0.1.1" } }, "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g=="],
|
"prom-client": ["prom-client@15.1.3", "", { "dependencies": { "@opentelemetry/api": "^1.4.0", "tdigest": "^0.1.1" } }, "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g=="],
|
||||||
|
|
||||||
"protobufjs": ["protobufjs@7.6.2", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-N9EiLovGEQOJSPF26Ij7qUGvahfEnq0eeYZ02aigIedkmz1qZSwjnP9SBITHJuF/6MYbIW4HDN8zdYjsjqJKXQ=="],
|
"protobufjs": ["protobufjs@7.6.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw=="],
|
||||||
|
|
||||||
"react": ["react@19.2.6", "", {}, "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q=="],
|
"react": ["react@19.2.7", "", {}, "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.2.6", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.6" } }, "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g=="],
|
"react-dom": ["react-dom@19.2.7", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.7" } }, "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ=="],
|
||||||
|
|
||||||
"react-is": ["react-is@19.2.7", "", {}, "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A=="],
|
"react-is": ["react-is@19.2.7", "", {}, "sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A=="],
|
||||||
|
|
||||||
"react-redux": ["react-redux@9.3.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g=="],
|
"react-redux": ["react-redux@9.3.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g=="],
|
||||||
|
|
||||||
"react-router": ["react-router@7.15.1", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A=="],
|
"react-router": ["react-router@7.17.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ=="],
|
||||||
|
|
||||||
"react-router-dom": ["react-router-dom@7.15.1", "", { "dependencies": { "react-router": "7.15.1" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg=="],
|
"react-router-dom": ["react-router-dom@7.17.0", "", { "dependencies": { "react-router": "7.17.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw=="],
|
||||||
|
|
||||||
"recharts": ["recharts@3.8.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="],
|
"recharts": ["recharts@3.8.1", "", { "dependencies": { "@reduxjs/toolkit": "^1.9.0 || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg=="],
|
||||||
|
|
||||||
@@ -601,12 +671,16 @@
|
|||||||
|
|
||||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||||
|
|
||||||
"rolldown": ["rolldown@1.0.2", "", { "dependencies": { "@oxc-project/types": "=0.132.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.2", "@rolldown/binding-darwin-arm64": "1.0.2", "@rolldown/binding-darwin-x64": "1.0.2", "@rolldown/binding-freebsd-x64": "1.0.2", "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", "@rolldown/binding-linux-arm64-gnu": "1.0.2", "@rolldown/binding-linux-arm64-musl": "1.0.2", "@rolldown/binding-linux-ppc64-gnu": "1.0.2", "@rolldown/binding-linux-s390x-gnu": "1.0.2", "@rolldown/binding-linux-x64-gnu": "1.0.2", "@rolldown/binding-linux-x64-musl": "1.0.2", "@rolldown/binding-openharmony-arm64": "1.0.2", "@rolldown/binding-wasm32-wasi": "1.0.2", "@rolldown/binding-win32-arm64-msvc": "1.0.2", "@rolldown/binding-win32-x64-msvc": "1.0.2" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g=="],
|
"rolldown": ["rolldown@1.0.3", "", { "dependencies": { "@oxc-project/types": "=0.133.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.3", "@rolldown/binding-darwin-arm64": "1.0.3", "@rolldown/binding-darwin-x64": "1.0.3", "@rolldown/binding-freebsd-x64": "1.0.3", "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", "@rolldown/binding-linux-arm64-gnu": "1.0.3", "@rolldown/binding-linux-arm64-musl": "1.0.3", "@rolldown/binding-linux-ppc64-gnu": "1.0.3", "@rolldown/binding-linux-s390x-gnu": "1.0.3", "@rolldown/binding-linux-x64-gnu": "1.0.3", "@rolldown/binding-linux-x64-musl": "1.0.3", "@rolldown/binding-openharmony-arm64": "1.0.3", "@rolldown/binding-wasm32-wasi": "1.0.3", "@rolldown/binding-win32-arm64-msvc": "1.0.3", "@rolldown/binding-win32-x64-msvc": "1.0.3" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
|
"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=="],
|
"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": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||||
@@ -621,7 +695,7 @@
|
|||||||
|
|
||||||
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
|
"tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="],
|
||||||
|
|
||||||
"tailwindcss": ["tailwindcss@4.3.0", "", {}, "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q=="],
|
"tailwindcss": ["tailwindcss@4.3.1", "", {}, "sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q=="],
|
||||||
|
|
||||||
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
"tapable": ["tapable@2.3.3", "", {}, "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A=="],
|
||||||
|
|
||||||
@@ -629,7 +703,7 @@
|
|||||||
|
|
||||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
|
|
||||||
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
|
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||||
|
|
||||||
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
"token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
|
||||||
|
|
||||||
@@ -637,7 +711,7 @@
|
|||||||
|
|
||||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"tsx": ["tsx@4.22.3", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg=="],
|
"tsx": ["tsx@4.22.4", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg=="],
|
||||||
|
|
||||||
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
"typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
|
||||||
|
|
||||||
@@ -649,7 +723,7 @@
|
|||||||
|
|
||||||
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
"victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
||||||
|
|
||||||
"vite": ["vite@8.0.14", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.2", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw=="],
|
"vite": ["vite@8.0.16", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.15", "rolldown": "1.0.3", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw=="],
|
||||||
|
|
||||||
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="],
|
"vite-tsconfig-paths": ["vite-tsconfig-paths@6.1.1", "", { "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", "tsconfck": "^3.0.3" }, "peerDependencies": { "vite": "*" } }, "sha512-2cihq7zliibCCZ8P9cKJrQBkfgdvcFkOOc3Y02o3GWUDLgqjWsZudaoiuOwO/gzTzy17cS5F7ZPo4bsnS4DGkg=="],
|
||||||
|
|
||||||
@@ -665,13 +739,27 @@
|
|||||||
|
|
||||||
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
|
||||||
|
|
||||||
"zustand": ["zustand@5.0.13", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-efI2tVaVQPqtOh114loML/Z80Y4NP3yc+Ff0fYiZJPauNeWZeIp/bRFD7I9bfmCOYBh/PHxlglQ9+wvlwnPikQ=="],
|
"zustand": ["zustand@5.0.14", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g=="],
|
||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||||
|
|
||||||
"@reduxjs/toolkit/immer": ["immer@11.1.8", "", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="],
|
"@reduxjs/toolkit/immer": ["immer@11.1.8", "", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="],
|
||||||
|
|
||||||
"tsx/esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="],
|
"@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=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
|
||||||
|
|
||||||
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"tsx/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="],
|
||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||||
|
|
||||||
@@ -717,56 +805,56 @@
|
|||||||
|
|
||||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="],
|
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.0", "", { "os": "android", "cpu": "arm" }, "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ=="],
|
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.0", "", { "os": "android", "cpu": "arm64" }, "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw=="],
|
"tsx/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.0", "", { "os": "android", "cpu": "x64" }, "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA=="],
|
"tsx/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q=="],
|
"tsx/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ=="],
|
"tsx/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q=="],
|
"tsx/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw=="],
|
"tsx/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw=="],
|
"tsx/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A=="],
|
"tsx/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ=="],
|
"tsx/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg=="],
|
"tsx/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w=="],
|
"tsx/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg=="],
|
"tsx/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.0", "", { "os": "linux", "cpu": "none" }, "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ=="],
|
"tsx/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q=="],
|
"tsx/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.0", "", { "os": "linux", "cpu": "x64" }, "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ=="],
|
"tsx/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw=="],
|
"tsx/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.0", "", { "os": "none", "cpu": "x64" }, "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw=="],
|
"tsx/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g=="],
|
"tsx/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA=="],
|
"tsx/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.0", "", { "os": "none", "cpu": "arm64" }, "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w=="],
|
"tsx/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw=="],
|
"tsx/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA=="],
|
"tsx/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA=="],
|
"tsx/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="],
|
||||||
|
|
||||||
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="],
|
"tsx/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,8 +38,6 @@ services:
|
|||||||
API_PORT: "3000"
|
API_PORT: "3000"
|
||||||
WEB_APP_URL: https://zeavisedu.asepharyana.my.id
|
WEB_APP_URL: https://zeavisedu.asepharyana.my.id
|
||||||
ML_SERVICE_URL: ${ML_SERVICE_URL:-http://zeavis-ml:8000}
|
ML_SERVICE_URL: ${ML_SERVICE_URL:-http://zeavis-ml:8000}
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
labels:
|
labels:
|
||||||
traefik.enable: "true"
|
traefik.enable: "true"
|
||||||
traefik.http.routers.zeavis-api.rule: Host(`api-zeavisedu.asepharyana.my.id`)
|
traefik.http.routers.zeavis-api.rule: Host(`api-zeavisedu.asepharyana.my.id`)
|
||||||
@@ -53,8 +51,6 @@ services:
|
|||||||
image: prom/node-exporter:v1.8.2
|
image: prom/node-exporter:v1.8.2
|
||||||
container_name: zeavis-node-exporter
|
container_name: zeavis-node-exporter
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
|
||||||
- "9100:9100"
|
|
||||||
command:
|
command:
|
||||||
- "--path.rootfs=/host"
|
- "--path.rootfs=/host"
|
||||||
- "--web.listen-address=:9100"
|
- "--web.listen-address=:9100"
|
||||||
@@ -77,8 +73,6 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
MODEL_PATH: /app/model/model.onnx
|
MODEL_PATH: /app/model/model.onnx
|
||||||
MODEL_INPUT_SIZE: "224"
|
MODEL_INPUT_SIZE: "224"
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
labels:
|
labels:
|
||||||
traefik.enable: "true"
|
traefik.enable: "true"
|
||||||
traefik.http.routers.zeavis-ml.rule: Host(`ml-zeavisedu.asepharyana.my.id`)
|
traefik.http.routers.zeavis-ml.rule: Host(`ml-zeavisedu.asepharyana.my.id`)
|
||||||
|
|||||||
+80
-33
@@ -1,6 +1,25 @@
|
|||||||
# Infra — ZeaVis Edu Multi-VPS Deployment
|
# Infrastruktur — ZeaVis Edu
|
||||||
|
|
||||||
## Arsitektur
|
> Arsitektur multi-VPS untuk deployment produksi ZeaVis Edu dengan Tailscale mesh VPN dan observabilitas penuh.
|
||||||
|
|
||||||
|
← [Kembali ke README utama](../README.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Daftar Isi
|
||||||
|
|
||||||
|
1. [Arsitektur](#1-arsitektur)
|
||||||
|
2. [Prasyarat GitHub Secrets](#2-prasyarat-github-secrets)
|
||||||
|
3. [Setup VPS](#3-setup-vps)
|
||||||
|
4. [Port yang Dibuka](#4-port-yang-dibuka)
|
||||||
|
5. [Metrics Flow](#5-metrics-flow)
|
||||||
|
6. [Perintah Penting](#6-perintah-penting)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Arsitektur
|
||||||
|
|
||||||
|
ZeaVis Edu berjalan di **dua VPS terpisah** yang terhubung melalui **Tailscale** mesh VPN:
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────┐ ┌──────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────┐ ┌──────────────────────────────────────────────┐
|
||||||
@@ -44,32 +63,41 @@
|
|||||||
└─────────────────────────────────────────────┘ └──────────────────────────────────────────────┘
|
└─────────────────────────────────────────────┘ └──────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
## Prerequisites
|
| VPS | Hostname | OS | Peran |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **App VPS** | `imrnes` | Arch Linux | Web (:80), API (:3000), ML Service (:8000) |
|
||||||
|
| **Telemetry VPS** | `orange` | Ubuntu | Prometheus, ClickHouse, Telemetry UI |
|
||||||
|
|
||||||
### GitHub Secrets (untuk CI/CD)
|
---
|
||||||
|
|
||||||
**App VPS deploy (`.github/workflows/deploy.yml`):**
|
## 2. Prasyarat GitHub Secrets
|
||||||
| Secret | Value |
|
|
||||||
|--------|-------|
|
### App VPS — `.github/workflows/deploy.yml`
|
||||||
|
|
||||||
|
| Secret | Keterangan |
|
||||||
|
|---|---|
|
||||||
| `VPS_HOST` | `100.108.1.124` (imrnes) |
|
| `VPS_HOST` | `100.108.1.124` (imrnes) |
|
||||||
| `VPS_USER` | `mytheclipse` |
|
| `VPS_USER` | `mytheclipse` |
|
||||||
| `VPS_SSH_KEY` | Private SSH key for imrnes |
|
| `VPS_SSH_KEY` | Private SSH key untuk imrnes |
|
||||||
| `VPS_PORT` | `22` |
|
| `VPS_PORT` | `22` |
|
||||||
| `DATABASE_URL` | PostgreSQL connection string |
|
| `DATABASE_URL` | PostgreSQL connection string |
|
||||||
| `SESSION_SECRET` | Random session secret |
|
| `SESSION_SECRET` | Random session secret |
|
||||||
|
|
||||||
**Telemetry VPS deploy (`.github/workflows/telemetry-ci-cd.yml`):**
|
### Telemetry VPS — `.github/workflows/telemetry-ci-cd.yml`
|
||||||
| Secret | Value |
|
|
||||||
|--------|-------|
|
| Secret | Keterangan |
|
||||||
|
|---|---|
|
||||||
| `TELEMETRY_VPS_HOST` | `100.96.248.86` (orange) |
|
| `TELEMETRY_VPS_HOST` | `100.96.248.86` (orange) |
|
||||||
| `TELEMETRY_VPS_USER` | SSH username for orange |
|
| `TELEMETRY_VPS_USER` | SSH username |
|
||||||
| `TELEMETRY_VPS_SSH_KEY` | Private SSH key for orange |
|
| `TELEMETRY_VPS_SSH_KEY` | Private SSH key |
|
||||||
| `TELEMETRY_VPS_PORT` | `22` |
|
| `TELEMETRY_VPS_PORT` | `22` |
|
||||||
| `GHCR_PAT` | GitHub PAT with `write:packages` + `read:packages` |
|
| `GHCR_PAT` | GitHub PAT dengan `write:packages` + `read:packages` |
|
||||||
|
|
||||||
### VPS Setup
|
---
|
||||||
|
|
||||||
#### 1. App VPS (imrnes — 100.108.1.124)
|
## 3. Setup VPS
|
||||||
|
|
||||||
|
### App VPS (imrnes — 100.108.1.124)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create Docker network
|
# Create Docker network
|
||||||
@@ -79,33 +107,35 @@ docker network create telemetry-net
|
|||||||
# ZeaVis Edu apps deploy automatically via GitHub Actions
|
# ZeaVis Edu apps deploy automatically via GitHub Actions
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. Telemetry VPS (orange — 100.96.248.86)
|
### Telemetry VPS (orange — 100.96.248.86)
|
||||||
|
|
||||||
Deploy via GitHub Actions workflow `.github/workflows/telemetry-ci-cd.yml`.
|
Deploy via GitHub Actions atau manual:
|
||||||
|
|
||||||
Atau manual:
|
|
||||||
```bash
|
```bash
|
||||||
ssh mytheclipse@100.96.248.86
|
ssh mytheclipse@100.96.248.86
|
||||||
mkdir -p /opt/telemetry
|
mkdir -p /opt/telemetry
|
||||||
# ... sync files from telemetry/ directory ...
|
|
||||||
cd /opt/telemetry
|
cd /opt/telemetry
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
bash clickhouse/init.sh
|
bash clickhouse/init.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
## Port yang dibuka
|
---
|
||||||
|
|
||||||
|
## 4. Port yang Dibuka
|
||||||
|
|
||||||
### App VPS (imrnes)
|
### App VPS (imrnes)
|
||||||
|
|
||||||
| Port | Service | Akses |
|
| Port | Service | Akses |
|
||||||
|------|---------|-------|
|
|---|---|---|
|
||||||
| 80/443 | Web (via Traefik/Coolify) | Public |
|
| 80/443 | Web (via Traefik/Coolify) | Public |
|
||||||
| 3000 | API metrics | Tailscale-only |
|
| 3000 | API metrics | Tailscale-only |
|
||||||
| 8000 | ML service metrics | Tailscale-only |
|
| 8000 | ML service metrics | Tailscale-only |
|
||||||
| 9100 | Node Exporter | Tailscale-only |
|
| 9100 | Node Exporter | Tailscale-only |
|
||||||
|
|
||||||
### Telemetry VPS (orange)
|
### Telemetry VPS (orange)
|
||||||
|
|
||||||
| Port | Service | Akses |
|
| Port | Service | Akses |
|
||||||
|------|---------|-------|
|
|---|---|---|
|
||||||
| 80/443 | Telemetry UI (via Coolify Traefik) | Public |
|
| 80/443 | Telemetry UI (via Coolify Traefik) | Public |
|
||||||
| 8181 | Telemetry UI (direct) | Tailscale-only |
|
| 8181 | Telemetry UI (direct) | Tailscale-only |
|
||||||
| 9090 | Prometheus | Tailscale-only |
|
| 9090 | Prometheus | Tailscale-only |
|
||||||
@@ -114,27 +144,44 @@ bash clickhouse/init.sh
|
|||||||
| 8123 | ClickHouse HTTP | Tailscale-only |
|
| 8123 | ClickHouse HTTP | Tailscale-only |
|
||||||
| 9000 | ClickHouse Native | Tailscale-only |
|
| 9000 | ClickHouse Native | Tailscale-only |
|
||||||
|
|
||||||
## Metrics Flow
|
---
|
||||||
|
|
||||||
1. **App services** expose `/metrics` pada port masing-masing
|
## 5. Metrics Flow
|
||||||
|
|
||||||
|
1. **App services** mengekspos `GET /metrics` di port masing-masing
|
||||||
2. **Prometheus** di orange VPS scrape via Tailscale IP (`100.108.1.124:PORT`)
|
2. **Prometheus** di orange VPS scrape via Tailscale IP (`100.108.1.124:PORT`)
|
||||||
3. **Prometheus** forward ke **Metric Ingester** via `remote_write`
|
3. Prometheus forward ke **Metric Ingester** via `remote_write`
|
||||||
4. **Metric Ingester** enrich → filter → forward ke **Vector**
|
4. Metric Ingester enrich → filter → forward ke **Vector**
|
||||||
5. **Vector** buffer → write ke **ClickHouse**
|
5. Vector buffer → write ke **ClickHouse**
|
||||||
6. **Telemetry UI** query via **Query Proxy** → **ClickHouse**
|
6. **Telemetry UI** query via **Query Proxy** → **ClickHouse**
|
||||||
|
|
||||||
## Useful Commands
|
```
|
||||||
|
App Services (/metrics)
|
||||||
|
│
|
||||||
|
▼ (scrape via Tailscale)
|
||||||
|
Prometheus ──(remote_write)──► Metric Ingester ──► Vector ──► ClickHouse
|
||||||
|
│
|
||||||
|
Query Proxy ◄── Telemetry UI
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Perintah Penting
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Telemetry stack status
|
# Status telemetry stack
|
||||||
make telemetry-status
|
make telemetry-status
|
||||||
|
|
||||||
# View telemetry logs
|
# Lihat log service tertentu
|
||||||
make telemetry-logs s=prometheus
|
make telemetry-logs s=prometheus
|
||||||
|
|
||||||
# Send test metric
|
# Kirim test metric
|
||||||
make telemetry-test-metric
|
make telemetry-test-metric
|
||||||
|
|
||||||
# Restart a service
|
# Restart service
|
||||||
make telemetry-restart s=vector
|
make telemetry-restart s=vector
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
← [Kembali ke README utama](../README.md) • [ML Service →](../apps/ml-service/README.md) • [Pipeline ML →](../Machine_Learning/README.md)
|
||||||
|
|||||||
+6
-1
@@ -13,5 +13,10 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
]
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "2.11.0",
|
||||||
|
"@tauri-apps/plugin-deep-link": "2.4.9",
|
||||||
|
"@tauri-apps/plugin-opener": "2.5.4"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export type RegisterRequest = AuthRequest & {
|
|||||||
export type AuthResponse = {
|
export type AuthResponse = {
|
||||||
user: AuthUser;
|
user: AuthUser;
|
||||||
features: AuthFeatures;
|
features: AuthFeatures;
|
||||||
|
token?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DiagnosisPrediction = {
|
export type DiagnosisPrediction = {
|
||||||
|
|||||||
Reference in New Issue
Block a user