Compare commits

..
27 Commits
Author SHA1 Message Date
MythEclipseandClaude 58d4cc0164 chore: update telemetry submodule and fix Makefile comment
Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 16:40:23 +07:00
MythEclipseandClaude a3105200e3 docs(claude): document Android Google OAuth fixes and design rules
Record the three bugs found during Android OAuth debugging,
their root causes, and the fix patterns to follow for future
Tauri deep-link handlers.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 16:37:58 +07:00
MythEclipseandClaude c75cba214e fix(android): resolve Google OAuth login flow on Tauri Android
Three interrelated fixes for the Android Google sign-in flow:

1. API base URL mismatch (404 error):
   - auth-form.tsx used 'window.location.origin || VITE_API_BASE_URL',
     which fell back to 'http://tauri.localhost' in Android WebView
     instead of the actual API server.
   - Fix: import shared 'apiBaseUrl' from api-client.ts (already had
     the correct fallback: 'https://zeavisedu.asepharyana.my.id').
   - Added .env with VITE_API_BASE_URL for dev mode resilience.

2. Deep-link caused IPC callback errors:
   - 'processDeepLinkUrl()' used window.location.href = target,
     triggering a full page reload that orphaned pending Tauri IPC
     promises, causing 'Cannot read properties of undefined (reading
     'runCallback')' errors.
   - Cold-start: keep get_current but use window.location.href (safe
     at boot — no SPA state to lose).
   - Warm-start: use sessionStorage + custom DOM event + React Router
     navigate() via new <DeepLinkRouterHandler /> layout route,
     avoiding any page reload.

3. SPA navigation did not trigger OAuth token handler:
   - LoginPage's useEffect for ?token=xxx depended only on
     [setUser, queryClient, navigate] — location.search changes
     from a SPA navigate() call were ignored.
   - Fix: added location.search and location to deps.
   - Added visibilitychange + focus listeners so returning from the
     Google auth browser always re-checks URL params.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 16:36:31 +07:00
MythEclipse c010d16aaa style(web): adjust card height and layout on scan page 2026-06-16 14:18:04 +07:00
MythEclipseandClaude 94ed062026 fix(infra): add compose port bindings for Prometheus scraping via Tailscale
Add ports mapping (api:3000, ml:8000, node_exporter:9100) bound to
TS_IP env var (default 0.0.0.0). Inject TS_IP via deploy GH Action.

Why: containers had no host port mapping → Prometheus on telemetry
VPS (imrnes) could not scrape metrics via Tailscale IP.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 14:15:18 +07:00
MythEclipse b4bd114eed fix(tauri): configure deep-link plugin for mobile custom scheme 2026-06-16 05:14:08 +07:00
MythEclipseandClaude 7423e4d83b fix(tauri): use raw __TAURI_INTERNALS__ invoke, fix dynamic import on Android
Root cause: dynamic import('@tauri-apps/plugin-opener') silently fails on
Android Tauri WebView because the module resolution path for @tauri-apps/api
(plugin dependency) differs from npm expectations in the bundled context.

Rewrote tauri.ts to use window.__TAURI_INTERNALS__.invoke() directly:
- openUrl() → invoke('plugin:opener|open_url', {url})
- setupDeepLinkHandler() → invoke('plugin:deep-link|get_current')
- Warm-start listener still uses import('@tauri-apps/api/event') for
  deep-link://new-url events (bundled as separate chunk by Vite)
- Added @tauri-apps/api as direct dependency

Also kept withGlobalTauri: true (needed for __TAURI_INTERNALS__ injection)
but reverted APK frontendDist back to bundled React app (../web/dist)
since redirect-to-live-web approach was unreliable.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 04:34:13 +07:00
MythEclipseandClaude 4b252a1d5d fix(tauri): enable withGlobalTauri so Tauri APIs work on live web
Without this, the redirect page loads zeavisedu.asepharyana.my.id but
__TAURI_INTERNALS__ is not injected, so isTauri() returns false,
handleGoogleLogin uses window.location.href (WebView navigation), and
Google blocks embedded WebView since 2016.

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-16 01:03:45 +07:00
30 changed files with 1346 additions and 210 deletions
+5 -1
View File
@@ -136,7 +136,11 @@ jobs:
rm -rf gen/android
bun tauri android init
- name: Patch AndroidManifest (CAMERA permission)
- name: Generate Android launcher icons from SVG logo
working-directory: apps/tauri
run: bun run scripts/generate-icons.js
- name: Patch AndroidManifest (CAMERA permission + deep link)
working-directory: apps/tauri
run: bash scripts/patch-android-manifest.sh
+1
View File
@@ -179,6 +179,7 @@ jobs:
SESSION_SECRET=${{ secrets.SESSION_SECRET }}
WEB_APP_URL=https://zeavisedu.asepharyana.my.id
ML_SERVICE_URL=http://zeavis-ml:8000
TS_IP=${{ vars.TS_IP || '100.96.248.86' }}
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
+45 -5
View File
@@ -142,19 +142,31 @@ make telemetry-down
## Telemetry architecture
The repository includes a full Prometheus → ClickHouse metric pipeline as a git submodule at `telemetry/`. Each ZeaVis Edu service exposes a `GET /metrics` endpoint:
The telemetry stack lives as a git submodule at `telemetry/` (repo `MythEclipse/Telemetry`). Architecture:
| Layer | Service | Role |
|-------|---------|------|
| Collector & Storage | **Prometheus** | Metric scraping & TSDB storage |
| System metrics | **Node Exporter** | CPU, memory, disk per host |
| Query | **Query Proxy** | REST API over Prometheus HTTP API |
| Visualization | **Grafana** | OSS dashboard & PromQL |
| Entry point | **Telemetry UI** | nginx + Vue 3 SPA |
Data flow: Node Exporter → Prometheus scrape (every 15s) → Grafana (PromQL) / Query Proxy (/api/metrics).
Each ZeaVis Edu service exposes a `GET /metrics` endpoint:
- **Web app** (`apps/web`): In dev mode, a Vite plugin serves client-side session metrics (page views, Web Vitals). In production, nginx proxies `/metrics` to the API service. Source: `apps/web/src/lib/telemetry.ts`, `apps/web/vite-plugin-metrics.ts`.
- **API** (`apps/api`): Uses `prom-client` for Node.js default metrics plus custom HTTP, auth, classification, and diagnosis counters/histograms. Source: `apps/api/src/lib/telemetry.ts`, exposed via `apps/api/src/routes/metrics.ts`.
- **ML service** (`apps/ml-service`): Uses the `prometheus` Rust crate for HTTP metrics, prediction counts, and model load status. Source: `apps/ml-service/src/telemetry.rs`.
All three share the `zeavis_` metric prefix and are scraped by the Telemetry Prometheus instance via `file_sd_configs` (see `telemetry/prometheus/targets/zeavis-edu.json`).
All three share the `zeavis_` metric prefix and are scraped by Prometheus via `file_sd_configs` (see `telemetry/prometheus/targets/zeavis-edu.json`).
**IMPORTANT — Production architecture:** ZeaVis Edu apps and the Telemetry stack run on **separate VPS instances** connected via **Tailscale** (mesh VPN). Prometheus scrapes the API and ML service through their **Tailscale IPs** (e.g. `100.x.x.a:3000`), not via Docker hostnames. The target file `telemetry/prometheus/targets/zeavis-edu.json` has `__CHANGE_ME__` placeholders — before deploying, replace with the actual Tailscale IPs of the app VPS.
**IMPORTANT — Production architecture:** ZeaVis Edu apps and the Telemetry stack run on **separate VPS instances** connected via **Tailscale** (mesh VPN). Prometheus scrapes the API and ML service through their **Tailscale IPs** (e.g. `100.x.x.a:3000`), not via Docker hostnames. The target file has `__CHANGE_ME__` placeholders — replace with actual Tailscale IPs before deploying.
The telemetry stack is managed from the project root via `make telemetry-*` targets (see `Makefile`). The Docker Compose files in `telemetry/deploy/` define 6 services (Prometheus, Metric Ingester, Vector, ClickHouse, Query Proxy, Telemetry UI).
The telemetry stack is managed from the project root via `make telemetry-*` targets (see `Makefile`). Docker Compose defines 5 services (Prometheus, Node Exporter, Query Proxy, Grafana, Telemetry UI).
For **local single-host dev**, Prometheus can reach app services via a shared Docker network (`app-shared-net`). Use `make telemetry-up-local` for this mode — it includes the `docker-compose.telemetry.yml` override.
For **local single-host dev**, Prometheus can reach app services via a shared Docker network (`app-shared-net`). Use `make telemetry-up-local` for this mode.
## Fullstack application architecture
@@ -191,6 +203,34 @@ The following files/directories are generated or externally supplied during the
- `Machine_Learning/best_model/best_model.keras` — trained model downloaded from Colab/Google Drive.
- `Machine_Learning/model/saved_model/`, `model/model.tflite`, `model/model.onnx`, and `model/tfjs_model/` — production exports.
## Android Google OAuth (Tauri) — known issues & fixes
The Tauri Android app uses Chrome's `intent://` protocol to bounce back from Google's OAuth browser page. Three bugs were found and fixed in commit `c75cba2`:
### 1. API base URL falls back to `http://tauri.localhost`
**Symptom:** Google login button navigates to `http://tauri.localhost/api/v1/auth/google` → 404.
**Root cause:** `auth-form.tsx` used `import.meta.env.VITE_API_BASE_URL || window.location.origin`. In Android WebView the origin is `http://tauri.localhost` (Vite dev server), not the API server.
**Fix:** Import shared `apiBaseUrl` from `api-client.ts` which already has the correct fallback: `import.meta.env.VITE_API_BASE_URL ?? 'https://zeavisedu.asepharyana.my.id'`.
### 2. `deep-link:get_current` IPC promise orphaned on SPA navigation
**Symptom:** `Cannot read properties of undefined (reading 'runCallback')` floods log; OAuth never completes.
**Root cause:** `plugin:deep-link|get_current` returns a JS promise that stays pending. When React Router's `navigate()` changes the URL (SPA, no page reload), the Tauri IPC bridge invalidates the pending callback reference — but the promise never resolves or rejects cleanly, so `.runCallback` is undefined.
**Fix (cold start):** `get_current` resolves via `window.location.href = target` (full reload). At boot there is no SPA state to lose, so a hard redirect is safe.
**Fix (warm start / `deep-link://new-url` event):** Store target in `sessionStorage` + dispatch a custom DOM event. A `<DeepLinkRouterHandler>` root layout route listens for the event and calls React Router's `navigate()`, keeping SPA state alive.
### 3. LoginPage `?token=` effect does not re-run on SPA navigation
**Symptom:** App navigates to `/login?token=xxx` but stays on the login form.
**Root cause:** The `useEffect` that reads `?token` and exchanges it for a session only listed `[setUser, queryClient, navigate]` as deps. React Router SPA navigation changes `location.search` but does not remount the component — so the effect never re-runs.
**Fix:** Added `location.search` to the effect's dependency array. Also added `visibilitychange` and `focus` event listeners as a backup — when the user returns from the Google OAuth browser tab, the app picks up the token from the URL even if the deep-link plugin's event was missed.
### Design rule for Tauri deep-link handlers
- **Cold start** (app was not running) → safe to use `window.location.href` (full reload). The React app has just booted, no state to lose.
- **Warm start** (app was running, user returns from system browser) → use React Router `navigate()` via custom events / sessionStorage. Do NOT use `window.location.href` — it triggers a full page unload which orphan Tauri IPC promises.
## Notes for future changes
- Keep README command examples and this file in sync when changing the ML pipeline.
+1 -1
View File
@@ -2,7 +2,7 @@
# ZeaVis Edu — Root Makefile
#
# Orchestrates the application stack (web, api, ml) and the telemetry
# metric pipeline (Prometheus → Ingester → Vector → ClickHouse).
# metric pipeline (Prometheus → Grafana).
#
# Telemetry commands operate on the submodule at telemetry/.
# =============================================================================
+58 -9
View File
@@ -4,7 +4,7 @@
<h1 align="center">ZeaVis Edu</h1>
<p align="center">
<strong>Asisten Edukasi Interaktif untuk Deteksi Penyakit Daun Jagung</strong><br>
<em>Computer Vision &bull; EfficientNetV2B0 &bull; Transfer Learning</em>
<em>Computer Vision &bull; EfficientNetV2B0 &bull; Rust ONNX Runtime &bull; Tauri 2 Android</em>
</p>
</p>
@@ -16,6 +16,7 @@
<a href="#-jadwal"><b>Jadwal</b></a> &bull;
<a href="#-tech-stack"><b>Tech Stack</b></a> &bull;
<a href="#-memulai"><b>Memulai</b></a> &bull;
<a href="#-platform"><b>Platform</b></a> &bull;
<a href="#-dokumentasi"><b>Dokumentasi</b></a>
</p>
@@ -79,7 +80,7 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
| Klasifikasi 3 penyakit + 1 daun sehat | Penyakit pada batang atau buah jagung |
| Deteksi berbasis unggah gambar daun | Prediksi tanpa input gambar |
| Rekomendasi obat & penanganan | Diagnosis pengganti ahli/POPT |
| Aplikasi web edukatif | Aplikasi mobile native |
| Aplikasi Web + Android (Tauri 2) | Aplikasi iOS |
### 4 Kelas yang Diklasifikasikan
@@ -98,7 +99,7 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
| 2 | **Model ML** | Model Computer Vision terlatih di Google Colab, siap produksi |
| 3 | **UI Antarmuka** | Front-End berbasis React + Vite dengan fitur unggah gambar |
| 4 | **Back-End Integration** | API + ML Service untuk inferensi real-time via Docker |
| 5 | **Prototipe Akhir** | Aplikasi web final dengan klasifikasi + modul edukasi (rekomendasi obat & penanganan) |
| 5 | **Prototipe Akhir** | Aplikasi Web + Android (Tauri 2) dengan klasifikasi & modul edukasi (rekomendasi obat & penanganan) |
---
@@ -131,7 +132,8 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
.
├── apps/
│ ├── api/ # Backend Elysia/Bun + Drizzle ORM + PostgreSQL
│ ├── ml-service/ # Rust/Axum + ONNX Runtime inference service
│ ├── ml-service/ # Rust/Axum + ONNX Runtime inference engine
│ ├── tauri/ # Tauri 2 mobile wrapper → Android APK
│ └── web/ # Frontend React + Vite + Tailwind CSS
├── Machine_Learning/ # Pipeline dataset, training Colab, ekspor model
│ └── README.md # ⤷ Panduan lengkap pipeline ML
@@ -147,8 +149,9 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
| 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 | Rust, Axum, ONNX Runtime | [`apps/ml-service/README.md`](apps/ml-service/README.md) |
| 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/` |
@@ -157,15 +160,18 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
## 🛠️ Tech Stack
### Frontend
### Frontend & Mobile
React &bull; Vite &bull; TypeScript &bull; React Router &bull; TanStack Query &bull; Zustand &bull; Tailwind CSS
**Tauri 2** (Android) &bull; Rust &bull; WebView &bull; Deep Link OAuth
### Backend API
Bun &bull; Elysia &bull; Drizzle ORM &bull; PostgreSQL &bull; prom-client
### Machine Learning
Python &bull; TensorFlow/Keras &bull; EfficientNetV2B0 &bull; Google Colab (GPU T4)
Rust &bull; Axum &bull; ONNX Runtime &bull; TFLite &bull; TensorFlow.js
### Inference Engine
**Rust** &bull; **Axum** &bull; **ONNX Runtime** &bull; TFLite &bull; TensorFlow.js
### DevOps & Infrastruktur
Docker &bull; Docker Compose &bull; Coolify &bull; Traefik &bull; Tailscale &bull; GitHub Actions (CI/CD)
@@ -181,7 +187,8 @@ Prometheus &bull; Metric Ingester (Go) &bull; Vector &bull; ClickHouse &bull; Qu
- **Bun** — runtime & package manager
- **Python 3.93.11** — pipeline ML
- **Rust & Cargo** — `apps/ml-service`
- **Rust & Cargo** — `apps/ml-service` (inference) & `apps/tauri` (Android)
- **Java 21 + Android SDK** — build Android APK
- **Docker & Docker Compose** — deployment & telemetry
- **PostgreSQL** — backend API
@@ -199,7 +206,9 @@ bun install
bun run dev # Semua service (web + api)
cd apps/web && bun run dev # Hanya frontend
cd apps/api && bun run start # Hanya backend API
cd apps/ml-service && cargo run # Hanya ML service (port 8000)
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
```
### Environment Variables
@@ -236,6 +245,46 @@ make telemetry-up # Telemetry stack
---
## 📱 Platform
ZeaVis Edu tersedia di **dua platform** dari satu codebase:
| Platform | Teknologi | Build |
|---|---|---|
| **Web** | React + Vite → Static SPA | `bun run build` |
| **Android** | Tauri 2 + Rust → WebView APK | `cd apps/tauri && bun run tauri android build --apk` |
### Tauri 2 Android
Aplikasi Android membungkus frontend web yang sama dalam **WebView native** menggunakan **Tauri 2**, memberikan akses ke API native Android tanpa menulis ulang UI.
**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
# 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
```
> Konfigurasi: `apps/tauri/tauri.conf.json` &bull; `apps/tauri/gen/android/`
---
## 📚 Dokumentasi
| Dokumen | Isi |
+35 -12
View File
@@ -64,23 +64,40 @@ function decodeGoogleIdToken(idToken: string): GoogleIdPayload {
}
/**
* Render a page for the Android system browser that redirects back to the
* Tauri app via a custom scheme (zeavisedu://). The app's AndroidManifest
* must register an intent filter for this scheme.
* 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 {
// Rewrite https://... to zeavisedu://... for the custom scheme
const deepLink = targetUrl.replace(/^https?:\/\//, 'zeavisedu://');
// Extract the path + query from the full URL for the intent
let pathAndQuery = '/login';
try {
const u = new URL(targetUrl);
pathAndQuery = u.pathname + u.search + u.hash;
} catch { /* use default */ }
const displayUrl = targetUrl.replace(/"/g, '&quot;');
const escapedPath = pathAndQuery.replace(/"/g, '&quot;');
// intent:// scheme: Chrome on Android opens the target app by package name
// browser_fallback_url: shown if the app isn't installed
// Use intent://login/... to produce data URI zeavisedu://login/login?token=xxx
// which new URL() can parse (single-slash non-hierarchical URLs break WebView)
const intentUrl = `intent://login${escapedPath}#Intent;scheme=zeavisedu;package=com.zeavis.edu;S.browser_fallback_url=${encodeURIComponent(targetUrl)};end`;
const html = `<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Kembali ke ZeaVis Edu</title></head>
<body style="font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f0fdf4">
<div style="text-align:center;padding:2rem">
<p style="color:#166534;font-size:1.1rem;margin-bottom:1.5rem">Login berhasil!<br>Kembali ke aplikasi...</p>
<a href="${deepLink.replace(/"/g, '&quot;')}" style="display:inline-block;background:#16a34a;color:white;padding:0.75rem 2rem;border-radius:0.5rem;text-decoration:none;font-weight:600;font-size:1rem">Buka ZeaVis Edu</a>
<p style="color:#6b7280;font-size:0.8rem;margin-top:1rem">Jika tombol tidak berfungsi, salin URL ini:<br><code style="word-break:break-all;font-size:0.75rem">${deepLink.replace(/</g, '&lt;')}</code></p>
<div style="text-align:center;padding:2rem;max-width:360px">
<p style="color:#166534;font-size:1.1rem;margin-bottom:1.5rem">Login Google berhasil!<br>Kembali ke aplikasi...</p>
<a href="${intentUrl.replace(/"/g, '&quot;')}" id="open-app" style="display:inline-block;background:#16a34a;color:white;padding:0.75rem 2rem;border-radius:0.5rem;text-decoration:none;font-weight:600;font-size:1rem;margin-bottom:1rem">Buka ZeaVis Edu</a>
<p style="color:#6b7280;font-size:0.8rem">Jika tombol di atas tidak berfungsi, salin dan buka URL ini di aplikasi ZeaVis Edu:</p>
<code style="display:block;word-break:break-all;font-size:0.7rem;color:#4b5563;background:#e5e7eb;padding:0.5rem;border-radius:0.25rem;margin-top:0.5rem">${displayUrl.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</code>
</div>
<script>window.location.href=${JSON.stringify(deepLink)};</script>
<script>
// Auto-open the intent
window.location.href = ${JSON.stringify(intentUrl)};
</script>
</body></html>`;
return new Response(html, {
status: 200,
@@ -297,12 +314,18 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
}
const token = await createSession(user.id);
set.headers['Set-Cookie'] = createSessionCookie(token, request.headers);
const sessionCookie = createSessionCookie(token, request.headers);
authCounter.labels('login', 'true').inc();
const successUrl = `${env.webAppUrl}/login?token=${encodeURIComponent(token)}`;
if (platform === 'tauri') return renderTauriDeepLinkPage(successUrl);
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) {
+616
View File
@@ -32,6 +32,137 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "async-broadcast"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
dependencies = [
"event-listener",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]]
name = "async-executor"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
dependencies = [
"async-task",
"concurrent-queue",
"fastrand",
"futures-lite",
"pin-project-lite",
"slab",
]
[[package]]
name = "async-io"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
dependencies = [
"autocfg",
"cfg-if",
"concurrent-queue",
"futures-io",
"futures-lite",
"parking",
"polling",
"rustix",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-lock"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
dependencies = [
"event-listener",
"event-listener-strategy",
"pin-project-lite",
]
[[package]]
name = "async-process"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
dependencies = [
"async-channel",
"async-io",
"async-lock",
"async-signal",
"async-task",
"blocking",
"cfg-if",
"event-listener",
"futures-lite",
"rustix",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "async-signal"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
dependencies = [
"async-io",
"async-lock",
"atomic-waker",
"cfg-if",
"futures-core",
"futures-io",
"rustix",
"signal-hook-registry",
"slab",
"windows-sys 0.61.2",
]
[[package]]
name = "async-task"
version = "4.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "atk"
version = "0.18.2"
@@ -127,6 +258,19 @@ dependencies = [
"objc2",
]
[[package]]
name = "blocking"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21"
dependencies = [
"async-channel",
"async-task",
"futures-io",
"futures-lite",
"piper",
]
[[package]]
name = "bs58"
version = "0.5.1"
@@ -295,6 +439,35 @@ dependencies = [
"memchr",
]
[[package]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-random"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
dependencies = [
"const-random-macro",
]
[[package]]
name = "const-random-macro"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
dependencies = [
"getrandom 0.2.17",
"once_cell",
"tiny-keccak",
]
[[package]]
name = "cookie"
version = "0.18.1"
@@ -378,6 +551,12 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -579,6 +758,15 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "dlv-list"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
dependencies = [
"const-random",
]
[[package]]
name = "dom_query"
version = "0.27.0"
@@ -665,6 +853,33 @@ version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
[[package]]
name = "endi"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
[[package]]
name = "enumflags2"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
dependencies = [
"enumflags2_derive",
"serde",
]
[[package]]
name = "enumflags2_derive"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -682,6 +897,37 @@ dependencies = [
"typeid",
]
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]]
name = "fastrand"
version = "2.4.1"
@@ -809,6 +1055,19 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-lite"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
dependencies = [
"fastrand",
"futures-core",
"futures-io",
"parking",
"pin-project-lite",
]
[[package]]
name = "futures-macro"
version = "0.3.32"
@@ -1147,6 +1406,12 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.5"
@@ -1174,6 +1439,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hex"
version = "0.4.3"
@@ -1459,6 +1730,25 @@ version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
[[package]]
name = "is-docker"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
dependencies = [
"once_cell",
]
[[package]]
name = "is-wsl"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
dependencies = [
"is-docker",
"once_cell",
]
[[package]]
name = "itoa"
version = "1.0.18"
@@ -1640,6 +1930,12 @@ dependencies = [
"libc",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.2"
@@ -2003,12 +2299,44 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "open"
version = "5.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c"
dependencies = [
"dunce",
"is-wsl",
"libc",
"pathdiff",
]
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-multimap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
dependencies = [
"dlv-list",
"hashbrown 0.14.5",
]
[[package]]
name = "ordered-stream"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
dependencies = [
"futures-core",
"pin-project-lite",
]
[[package]]
name = "pango"
version = "0.18.3"
@@ -2034,6 +2362,12 @@ dependencies = [
"system-deps",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -2057,6 +2391,12 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "pathdiff"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -2122,6 +2462,17 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "piper"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
dependencies = [
"atomic-waker",
"fastrand",
"futures-io",
]
[[package]]
name = "pkg-config"
version = "0.3.33"
@@ -2167,6 +2518,20 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "polling"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "potential_utf"
version = "0.1.5"
@@ -2399,6 +2764,16 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rust-ini"
version = "0.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
dependencies = [
"cfg-if",
"ordered-multimap",
]
[[package]]
name = "rustc-hash"
version = "2.1.2"
@@ -2414,6 +2789,19 @@ dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.13.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -2690,6 +3078,16 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "simd-adler32"
version = "0.3.9"
@@ -3042,6 +3440,65 @@ dependencies = [
"tauri-utils",
]
[[package]]
name = "tauri-plugin"
version = "2.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e"
dependencies = [
"anyhow",
"glob",
"plist",
"schemars 0.8.22",
"serde",
"serde_json",
"tauri-utils",
"walkdir",
]
[[package]]
name = "tauri-plugin-deep-link"
version = "2.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ee75bc5627f77bfdf40c913255ebc258117b10ebe2b2239a1a1cf40b0b58aa"
dependencies = [
"dunce",
"plist",
"rust-ini",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"tauri-utils",
"thiserror 2.0.18",
"tracing",
"url",
"windows-registry",
"windows-result 0.3.4",
]
[[package]]
name = "tauri-plugin-opener"
version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29"
dependencies = [
"dunce",
"glob",
"objc2-app-kit",
"objc2-foundation",
"open",
"schemars 0.8.22",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.18",
"url",
"windows",
"zbus",
]
[[package]]
name = "tauri-runtime"
version = "2.11.2"
@@ -3141,6 +3598,19 @@ dependencies = [
"toml 1.1.2+spec-1.1.0",
]
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.2",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "tendril"
version = "0.5.0"
@@ -3221,6 +3691,15 @@ dependencies = [
"time-core",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tinystr"
version = "0.8.3"
@@ -3445,9 +3924,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-attributes",
"tracing-core",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
@@ -3497,6 +3988,17 @@ version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "uds_windows"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.61.2",
]
[[package]]
name = "unic-char-property"
version = "0.9.0"
@@ -4033,6 +4535,17 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-registry"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
dependencies = [
"windows-link 0.1.3",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@@ -4457,6 +4970,67 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zbus"
version = "5.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285"
dependencies = [
"async-broadcast",
"async-executor",
"async-io",
"async-lock",
"async-process",
"async-recursion",
"async-task",
"async-trait",
"blocking",
"enumflags2",
"event-listener",
"futures-core",
"futures-lite",
"hex",
"libc",
"ordered-stream",
"rustix",
"serde",
"serde_repr",
"tracing",
"uds_windows",
"uuid",
"windows-sys 0.61.2",
"winnow 1.0.3",
"zbus_macros",
"zbus_names",
"zvariant",
]
[[package]]
name = "zbus_macros"
version = "5.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6"
dependencies = [
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
"zbus_names",
"zvariant",
"zvariant_utils",
]
[[package]]
name = "zbus_names"
version = "4.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
dependencies = [
"serde",
"winnow 1.0.3",
"zvariant",
]
[[package]]
name = "zeavis-edu-tauri"
version = "0.1.0"
@@ -4465,6 +5039,8 @@ dependencies = [
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-deep-link",
"tauri-plugin-opener",
]
[[package]]
@@ -4526,3 +5102,43 @@ name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zvariant"
version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0"
dependencies = [
"endi",
"enumflags2",
"serde",
"winnow 1.0.3",
"zvariant_derive",
"zvariant_utils",
]
[[package]]
name = "zvariant_derive"
version = "5.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737"
dependencies = [
"proc-macro-crate 3.5.0",
"proc-macro2",
"quote",
"syn 2.0.117",
"zvariant_utils",
]
[[package]]
name = "zvariant_utils"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6"
dependencies = [
"proc-macro2",
"quote",
"serde",
"syn 2.0.117",
"winnow 1.0.3",
]
+3
View File
@@ -12,5 +12,8 @@
},
"devDependencies": {
"@tauri-apps/cli": "^2"
},
"dependencies": {
"sharp": "0.35.1"
}
}
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env node
/**
* Generate Android launcher icons from the ZeaVis Edu logo SVG.
* Produces PNGs at all required densities and replaces Tauri's default icons.
*
* Usage: node scripts/generate-icons.js
* Requires: bun add sharp (already in devDependencies)
*/
const sharp = require('sharp');
const fs = require('fs');
const path = require('path');
const LOGO = path.resolve(__dirname, '../../../.github/assets/zeavis-logo.svg');
const RES = path.resolve(__dirname, '../gen/android/app/src/main/res');
// Android density buckets: [folder, size]
const DENSITIES = [
['mipmap-mdpi', 48],
['mipmap-hdpi', 72],
['mipmap-xhdpi', 96],
['mipmap-xxhdpi', 144],
['mipmap-xxxhdpi', 192],
];
async function generate() {
if (!fs.existsSync(LOGO)) {
console.error(`ERROR: Logo not found at ${LOGO}`);
process.exit(1);
}
console.log(`Generating icons from ${LOGO}...`);
for (const [folder, size] of DENSITIES) {
const dir = path.join(RES, folder);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
const png = await sharp(LOGO)
.resize(size, size, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
.png()
.toBuffer();
// Write both legacy and adaptive icon names
for (const name of ['ic_launcher.png', 'ic_launcher_foreground.png', 'ic_launcher_round.png']) {
fs.writeFileSync(path.join(dir, name), png);
}
console.log(` ${folder}: ${size}x${size} OK`);
}
// Also write the legacy icon to drawable for completeness
const drawableDir = path.join(RES, 'drawable');
if (!fs.existsSync(drawableDir)) fs.mkdirSync(drawableDir, { recursive: true });
const refPng = await sharp(LOGO)
.resize(144, 144, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
.png()
.toBuffer();
fs.writeFileSync(path.join(drawableDir, 'ic_launcher.png'), refPng);
console.log('Done. Android launcher icons generated.');
}
generate().catch((err) => {
console.error(err);
process.exit(1);
});
+10 -2
View File
@@ -10,7 +10,7 @@
"beforeDevCommand": "cd ../web && bun run dev"
},
"app": {
"withGlobalTauri": false,
"withGlobalTauri": true,
"windows": [
{
"title": "ZeaVis Edu",
@@ -26,5 +26,13 @@
"active": true,
"targets": "all"
},
"plugins": {}
"plugins": {
"deep-link": {
"mobile": [
{
"scheme": ["zeavisedu"]
}
]
}
}
}
+44
View File
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>ZeaVis Edu</title>
<script>
var LIVE = 'https://zeavisedu.asepharyana.my.id';
var T = window.__TAURI_INTERNALS__;
function navigate(path) {
window.location.replace(LIVE + path);
}
// On cold start, check if the app was opened via a deep link (Google OAuth)
// before redirecting to the live web app.
if (T && T.invoke) {
T.invoke('plugin:deep-link|get_current')
.then(function(urls) {
if (urls && urls.length > 0 && urls[0]) {
try {
var u = new URL(urls[0]);
var target = u.pathname + u.search + u.hash;
if (target && target !== '/') {
// Preserve full path + query (e.g. /login?token=xxx)
navigate(target);
return;
}
} catch (e) { /* malformed URL — fall through */ }
}
// No deep link — redirect to live app home
navigate('/');
})
.catch(function() { navigate('/'); });
} else {
// Not in Tauri (dev mode or unknown) — redirect to live
navigate('/');
}
</script>
</head>
<body style="background:#f0fdf4;font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0">
<p style="color:#16a34a">Memuat ZeaVis Edu...</p>
</body>
</html>
+2
View File
@@ -12,6 +12,8 @@
"dependencies": {
"@radix-ui/react-slot": "^1.2.4",
"@tanstack/react-query": "^5.100.11",
"@tauri-apps/plugin-deep-link": "^2",
"@tauri-apps/plugin-opener": "^2",
"@vitejs/plugin-react": "^6.0.2",
"@zeavis/shared": "workspace:*",
"class-variance-authority": "^0.7.1",
+45 -4
View File
@@ -3,6 +3,8 @@ import {
createBrowserRouter,
RouterProvider,
Navigate,
Outlet,
useNavigate,
} from "react-router-dom";
import { AuthInitializer } from "@/components/auth-initializer";
import { AuthGuard } from "@/components/auth-guard";
@@ -18,9 +20,10 @@ import { TelemetryPage } from "@/pages/telemetry-page";
import { LoginPage } from "@/pages/login-page";
import { RegisterPage } from "@/pages/register-page";
import { MainLayout } from "@/components/layout/main-layout";
import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { useAuthStore } from "@/store/auth-store";
import { apiClient } from "@/lib/api-client";
import { setupDeepLinkHandler, consumeDeepLinkTarget } from "@/lib/tauri";
function LogoutProses() {
const setUser = useAuthStore((state) => state.setUser);
@@ -42,6 +45,9 @@ import { trackPageView, trackError } from "./lib/telemetry";
const queryClient = new QueryClient();
const router = createBrowserRouter([
{
element: <DeepLinkRouterHandler />,
children: [
{ path: "/", element: <Navigate to="/login" replace /> },
{ path: "/login", element: <LoginPage /> },
{ path: "/register", element: <RegisterPage /> },
@@ -127,9 +133,7 @@ const router = createBrowserRouter([
},
{
path: "/logout",
element: (
<LogoutProses />
),
element: <LogoutProses />,
},
{
path: "/telemetry",
@@ -139,8 +143,40 @@ const router = createBrowserRouter([
</MainLayout>
),
},
],
},
]);
/**
* Listens for deep-link custom events and routes via React Router's navigate(),
* avoiding full page reloads that break the Tauri IPC bridge.
*/
function DeepLinkRouterHandler() {
const navigate = useNavigate();
const handled = useRef(new Set<string>());
useEffect(() => {
// Check for cold-start pending deep link
const pending = consumeDeepLinkTarget();
if (pending && !handled.current.has(pending)) {
handled.current.add(pending);
navigate(pending, { replace: true });
}
// Listen for warm-start deep links
const handler = (e: CustomEvent<string>) => {
const target = e.detail;
if (handled.current.has(target)) return;
handled.current.add(target);
navigate(target, { replace: true });
};
window.addEventListener('zeavis:deeplink', handler as EventListener);
return () => window.removeEventListener('zeavis:deeplink', handler as EventListener);
}, [navigate]);
return <Outlet />;
}
function PageViewTracker() {
const location = window.location;
useEffect(() => {
@@ -161,6 +197,11 @@ function GlobalErrorTracker() {
}
export function App() {
// Register deep link handler for Android OAuth return
useEffect(() => {
setupDeepLinkHandler();
}, []);
return (
<QueryClientProvider client={queryClient}>
<AuthInitializer />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

+7 -9
View File
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { apiBaseUrl } from '@/lib/api-client';
import { isTauri, openUrl } from '@/lib/tauri';
type AuthFormProps = {
@@ -29,20 +30,17 @@ export function AuthForm({ mode, isSubmitting, error, googleOAuthEnabled, onSubm
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}`;
const googleUrl = `${apiBaseUrl}/api/v1/auth/google?platform=${platform}`;
await openUrl(googleUrl);
}, []);
return (
<Card className="mx-auto w-full max-w-md">
<CardHeader>
<CardTitle>{mode === 'login' ? 'Masuk ke ZeaVis Edu' : 'Buat akun ZeaVis Edu'}</CardTitle>
<CardDescription>
<Card className="mx-auto w-full max-w-md bg-transparent border-none shadow-none">
<CardHeader className="text-center space-y-2">
<CardTitle className="text-2xl font-bold text-emerald-900">{mode === 'login' ? 'Masuk Akun ZeaVis Edu' : 'Buat akun ZeaVis Edu'}</CardTitle>
<CardDescription className="text-sm text-emerald-800/80">
{mode === 'login'
? 'Masuk untuk melihat riwayat diagnosis daun jagung Anda.'
? 'Masuk untuk menyimpan diagnosis dan mengikuti review pakar.'
: 'Daftar untuk menyimpan diagnosis dan mengikuti review pakar.'}
</CardDescription>
</CardHeader>
+1 -1
View File
@@ -14,7 +14,7 @@ import type {
} from '@zeavis/shared';
import { recordApiCall } from './telemetry';
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'https://zeavisedu.asepharyana.my.id';
export const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'https://zeavisedu.asepharyana.my.id';
const AUTH_TOKEN_KEY = 'zeavis_auth_token';
+92 -5
View File
@@ -1,7 +1,13 @@
/**
* Lightweight Tauri environment detection and utilities.
* Avoids importing @tauri-apps/api at module level so the web build
* doesn't bundle Tauri internals.
* Uses raw __TAURI_INTERNALS__ IPC to avoid bundling/import issues on Android.
*
* Deep-link flow (no full page reloads uses React Router navigate()):
* 1. Tauri deep-link plugin receives URL via intent/custom-scheme.
* 2. processDeepLinkUrl stores the target path in sessionStorage +
* dispatches a custom DOM event.
* 3. <DeepLinkRouterHandler /> inside <RouterProvider> picks it up and
* calls navigate(), keeping the React app alive.
*/
let _isTauri: boolean | null = null;
@@ -14,12 +20,93 @@ export function isTauri(): boolean {
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()) {
window.location.href = url;
return;
}
// Lazy-import Tauri opener only in Tauri context
const { openUrl: tauriOpenUrl } = await import('@tauri-apps/plugin-opener');
await tauriOpenUrl(url);
try {
const invoke = tauriInvoke();
await invoke('plugin:opener|open_url', { url });
} catch (err) {
console.error('Tauri openUrl failed, trying fallback:', err);
window.location.href = url;
}
}
// ── Deep link handling (no full reload) ─────────────────────────────────
const DEEP_LINK_KEY = 'zeavis_pending_deeplink';
const DEEP_LINK_EVENT = 'zeavis:deeplink';
/** Store a target path for the React Router to pick up without page reload. */
function storeDeepLinkTarget(target: string): void {
try { sessionStorage.setItem(DEEP_LINK_KEY, target); } catch { /* ignore */ }
}
/** Read and clear the stored deep link target. */
export function consumeDeepLinkTarget(): string | null {
try {
const v = sessionStorage.getItem(DEEP_LINK_KEY);
if (v) sessionStorage.removeItem(DEEP_LINK_KEY);
return v;
} catch { return null; }
}
export async function setupDeepLinkHandler(): Promise<void> {
if (!isTauri()) return;
try {
const invoke = tauriInvoke();
// Cold-start: app opened via intent:// (e.g. from Google OAuth callback).
// Use window.location.href for this (full page reload) — at cold start there
// is no SPA state to lose, so redirecting via location.href avoids orphaned
// IPC promises that cause "Cannot read properties of undefined (reading 'runCallback')".
invoke('plugin:deep-link|get_current')
.then((urls: any) => {
if (!urls?.[0]) return;
const target = extractDeepLinkTarget(urls[0]);
if (target && target !== window.location.pathname + window.location.search + window.location.hash) {
window.location.href = target;
}
})
.catch(() => {});
// Warm-start: listen for new URLs (already running app).
// Use React Router navigate() here since we have SPA state.
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) {
const target = extractDeepLinkTarget(url);
if (target) {
storeDeepLinkTarget(target);
window.dispatchEvent(new CustomEvent(DEEP_LINK_EVENT, { detail: target }));
}
}
});
} catch (err) {
console.error('Tauri deep-link setup failed:', err);
}
}
/** Extract path+query+hash from a deep-link URL. */
function extractDeepLinkTarget(url: string): string {
try {
const u = new URL(url);
return u.pathname + u.search + u.hash;
} catch {
let m = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
if (!m) m = url.match(/^[^:]+:\/(\/.*)?$/);
return m?.[1] ?? '';
}
}
+3 -3
View File
@@ -54,12 +54,12 @@ export function CatalogPage() {
<div className="space-y-6 max-w-5xl mx-auto pb-10">
{/* Page Title */}
<div>
<h1 className="text-2xl md:text-3xl font-extrabold text-[#214B11]">
<h1 className="text-2xl font-bold text-emerald-800">
Pustaka Penyakit
</h1>
<p className="mt-1 text-muted-foreground text-sm md:text-base">
<p className="text-gray-500 mt-1 text-md">
Referensi lengkap penyakit dan kondisi daun jagung yang dapat
dideteksi oleh sistem AI ZeaVis Edu.
dideteksi oleh sistem AI ZeaVis Edu
</p>
</div>
+35 -21
View File
@@ -6,7 +6,6 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useUiStore } from "@/store/ui-store";
import { apiClient } from "@/lib/api-client";
import bg from "@/assets/images/dashboard-bg.webp";
export function DashboardPage() {
const { dashboardCompact } = useUiStore();
@@ -23,7 +22,8 @@ export function DashboardPage() {
const summary = summaryQuery.data;
const diseases = diseasesQuery.data ?? [];
const diseasesQuick = useMemo(() =>
const diseasesQuick = useMemo(
() =>
diseases
.sort((a, b) => a.displayOrder - b.displayOrder)
.map((d) => ({
@@ -32,7 +32,7 @@ export function DashboardPage() {
color: d.accentColor,
slug: d.slug,
})),
[diseases]
[diseases],
);
const missionCards = [
@@ -74,7 +74,11 @@ export function DashboardPage() {
{/* Hero header */}
<header
className="relative overflow-hidden rounded-3xl bg-cover bg-center bg-no-repeat shadow-sm"
style={{ backgroundImage: `url(${bg})` }}
style={{
backgroundImage: `url(https://cdn.pixabay.com/photo/2014/09/09/19/07/corn-field-440338_1280.jpg)`,
backgroundPosition: "bottom",
backgroundSize: "cover",
}}
>
<div className="absolute inset-0 bg-gradient-to-b from-[#2F6E1A]/60 to-black/30" />
<div className="relative z-10 flex flex-col md:flex-row items-center justify-between gap-6 p-6 md:p-10">
@@ -82,7 +86,9 @@ export function DashboardPage() {
<span className="inline-block rounded-full bg-[#1E8A2A]/80 px-4 py-2 text-xs font-semibold">
AI FOR SMART EDUCATION
</span>
<h1 className="text-2xl md:text-4xl font-extrabold">Selamat Datang di</h1>
<h1 className="text-2xl md:text-4xl font-extrabold">
Selamat Datang di
</h1>
<h2 className="text-2xl md:text-4xl font-extrabold tracking-tight text-[#9AD872]">
ZeaVis Edu
</h2>
@@ -107,10 +113,7 @@ export function DashboardPage() {
variant="outline"
className="px-4 md:px-6 py-3 md:py-6 text-sm md:text-lg font-semibold text-white hover:bg-[#1E8A2A]"
>
<Link
to="/catalog"
className="inline-flex items-center gap-2"
>
<Link to="/catalog" className="inline-flex items-center gap-2">
Pustaka Penyakit
<ChevronRight className="h-5 w-6" />
</Link>
@@ -169,7 +172,10 @@ export function DashboardPage() {
<div className="text-3xl font-bold">
{summary.imageClassificationCount}
</div>
<Link to="/diagnoses" className="text-emerald-600 ml-auto hover:underline">
<Link
to="/diagnoses"
className="text-emerald-600 ml-auto hover:underline"
>
Lihat daftar
</Link>
</CardContent>
@@ -186,7 +192,10 @@ export function DashboardPage() {
<div className="text-3xl font-bold text-amber-600">
{summary.needsReviewCount}
</div>
<Link to="/diagnoses?status=needs_review" className="text-amber-600 ml-auto hover:underline">
<Link
to="/diagnoses?status=needs_review"
className="text-amber-600 ml-auto hover:underline"
>
Lihat daftar
</Link>
</CardContent>
@@ -200,10 +209,11 @@ export function DashboardPage() {
</CardTitle>
</CardHeader>
<CardContent className="h-full flex flex-col justify-start pt-2">
<div className="text-3xl font-bold text-red-600">
</div>
<Link to="/diagnoses?status=failed" className="text-red-600 ml-auto hover:underline">
<div className="text-3xl font-bold text-red-600"></div>
<Link
to="/diagnoses?status=failed"
className="text-red-600 ml-auto hover:underline"
>
Lihat daftar
</Link>
</CardContent>
@@ -220,7 +230,10 @@ export function DashboardPage() {
<div className="text-3xl font-bold text-red-600">
{summary.riskDistribution.high}
</div>
<Link to="/catalog?risk=high" className="text-red-600 ml-auto hover:underline">
<Link
to="/catalog?risk=high"
className="text-red-600 ml-auto hover:underline"
>
Lihat pustaka
</Link>
</CardContent>
@@ -276,7 +289,8 @@ export function DashboardPage() {
Penyakit yang Dapat Dideteksi
</h3>
<p className="text-[15px] font-normal text-muted-foreground">
{diseases.length} kelas penyakit dan kondisi daun jagung dalam sistem kami
{diseases.length} kelas penyakit dan kondisi daun jagung dalam
sistem kami
</p>
</div>
<Link
@@ -295,9 +309,7 @@ export function DashboardPage() {
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
{diseasesQuick.map((d) => (
<Link key={d.slug} to={`/catalog/${d.slug}`}>
<Card
className="rounded-2xl bg-white p-4 shadow-sm h-full transition-transform hover:scale-[1.03] hover:shadow-md cursor-pointer"
>
<Card className="rounded-2xl bg-white p-4 shadow-sm h-full transition-transform hover:scale-[1.03] hover:shadow-md cursor-pointer">
<CardContent className="h-full p-4 flex flex-col justify-between">
<div className="flex items-start gap-3">
<div
@@ -324,7 +336,9 @@ export function DashboardPage() {
{/* Scan quick access */}
<div className="mt-12 flex flex-col sm:flex-row items-center gap-4 sm:gap-6 bg-[#1E8A2A] rounded-3xl p-5 sm:p-6">
<div className="flex-1 text-white">
<h3 className="text-2xl font-bold">Siap Mendeteksi Penyakit Daun?</h3>
<h3 className="text-2xl font-bold">
Siap Mendeteksi Penyakit Daun?
</h3>
<p className="text-sm text-[#9AD872] font-normal mt-2">
Unggah foto daun jagung Anda dan dapatkan hasil analisis AI
dalam hitungan detik.
+1 -1
View File
@@ -75,7 +75,7 @@ export function DiagnosesPage() {
<div className="space-y-6">
<div className="flex items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-emerald-800">Diagnosa Tanaman</h1>
<h1 className="text-2xl font-bold text-emerald-800">Diagnosa Penyakit</h1>
<p className="text-gray-500 mt-1 text-md">
Lihat hasil diagnosa dari scan yang telah dilakukan
</p>
+1 -1
View File
@@ -71,7 +71,7 @@ export function ExpertReviewsPage() {
<div className="space-y-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
<div>
<h1 className="text-2xl font-bold text-emerald-800">Review Pakar</h1>
<h1 className="text-2xl font-bold text-emerald-800">Tinjauan Pakar</h1>
<p className="text-gray-500 mt-1 text-md">
Tinjau hasil diagnosa dari scan yang telah dilakukan dan berikan
feedback untuk meningkatkan akurasi sistem AI ZeaVis Edu
+64 -7
View File
@@ -1,9 +1,10 @@
import { useState, useEffect, useRef } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Link, useNavigate, useLocation } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AuthForm } from "@/components/auth-form";
import { apiClient, setAuthToken } from "@/lib/api-client";
import { useAuthStore } from "@/store/auth-store";
import { isTauri } from "@/lib/tauri";
function getUrlParam(name: string): string | null {
return new URLSearchParams(window.location.search).get(name);
@@ -13,11 +14,13 @@ export function LoginPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const setUser = useAuthStore((state) => state.setUser);
const location = useLocation();
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>
// Must re-run on location.search change (SPA navigates to /login?token=xxx)
useEffect(() => {
const token = getUrlParam("token");
if (!token || oauthTokenConsumed.current) return;
@@ -39,7 +42,36 @@ export function LoginPage() {
setOauthProcessing(false);
setError(err instanceof Error ? err.message : "Google login gagal");
});
}, [setUser, queryClient, navigate]);
}, [setUser, queryClient, navigate, location.search]);
// Backup: when app returns from background (e.g. after Google OAuth browser)
// re-check URL params — the deep-link event may have been missed.
useEffect(() => {
if (!isTauri()) return;
if (getUrlParam("token") || oauthTokenConsumed.current) return;
const onVisibility = () => {
if (document.visibilityState !== "visible") return;
const token = getUrlParam("token");
if (token && !oauthTokenConsumed.current) {
setOauthProcessing(true);
}
};
const onFocus = () => {
const token = getUrlParam("token");
if (token && !oauthTokenConsumed.current) {
setOauthProcessing(true);
}
};
document.addEventListener("visibilitychange", onVisibility);
window.addEventListener("focus", onFocus);
return () => {
document.removeEventListener("visibilitychange", onVisibility);
window.removeEventListener("focus", onFocus);
};
}, []);
// Show OAuth error from query param
const oauthError = getUrlParam("error");
@@ -48,6 +80,13 @@ export function LoginPage() {
queryFn: () => apiClient.getMe(),
});
// Already authenticated — redirect to dashboard
useEffect(() => {
if (!meQuery.isLoading && meQuery.data?.user) {
navigate("/dashboard", { replace: true });
}
}, [meQuery.data, meQuery.isLoading, navigate]);
const mutation = useMutation({
mutationFn: apiClient.login,
onSuccess: (response) => {
@@ -65,15 +104,29 @@ export function LoginPage() {
<main className="flex min-h-screen items-center justify-center px-6 py-12">
<div className="flex flex-col items-center gap-3">
<div className="h-10 w-10 border-4 border-green-500 border-t-transparent rounded-full animate-spin" />
<p className="text-gray-500 text-sm">Menyelesaikan login dengan Google...</p>
<p className="text-gray-500 text-sm">
Menyelesaikan login dengan Google...
</p>
</div>
</main>
);
}
return (
<main className="flex min-h-screen items-center justify-center px-6 py-12">
<div className="w-full max-w-sm md:max-w-md space-y-4">
<main className="relative flex min-h-screen items-center justify-center px-6 py-12">
{/* Background Image with Overlay */}
<div
className="absolute inset-0 z-0"
style={{
backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), url(https://cdn.pixabay.com/photo/2014/09/09/19/07/corn-field-440338_1280.jpg)`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
/>
{/* Glassmorphism Container */}
<div className="relative z-10 w-full max-w-sm md:max-w-md space-y-6 bg-white/60 backdrop-blur-md p-8 md:p-10 rounded-3xl shadow-2xl border border-white/50">
<AuthForm
mode="login"
isSubmitting={mutation.isPending}
@@ -87,9 +140,13 @@ export function LoginPage() {
}}
onFieldChange={() => setError(null)}
/>
<p className="text-center text-sm text-muted-foreground">
<p className="text-center text-sm text-slate-700">
Belum punya akun?{" "}
<Link className="text-primary" to="/register">
<Link
className="text-emerald-700 font-bold hover:underline"
to="/register"
>
Daftar
</Link>
</p>
+12 -1
View File
@@ -28,7 +28,18 @@ export function RegisterPage() {
return (
<main className="flex min-h-screen items-center justify-center px-6 py-12">
<div className="w-full max-w-sm md:max-w-md space-y-4">
{/* Background Image with Overlay */}
<div
className="absolute inset-0 z-0"
style={{
backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), url(https://cdn.pixabay.com/photo/2014/09/09/19/07/corn-field-440338_1280.jpg)`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
/>
{/* Glassmorphism Container */}
<div className="relative z-10 w-full max-w-sm md:max-w-md space-y-6 bg-white/60 backdrop-blur-md p-8 md:p-10 rounded-3xl shadow-2xl border border-white/50">
<AuthForm
mode="register"
isSubmitting={mutation.isPending}
+12 -11
View File
@@ -36,8 +36,7 @@ export function ScanPage() {
// Camera mode state
const [useCamera, setUseCamera] = useState(false);
const handleCameraCapture = useCallback(
(file: File) => {
const handleCameraCapture = useCallback((file: File) => {
setFileName(file.name);
const url = URL.createObjectURL(file);
setPreviewUrl(url);
@@ -48,9 +47,7 @@ export function ScanPage() {
};
img.src = url;
setUseCamera(false);
},
[],
);
}, []);
const mutation = useMutation({
mutationFn: (file: File) => apiClient.createDiagnosis(file),
@@ -107,10 +104,10 @@ export function ScanPage() {
{/* Main Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-4">
<div>
<h1 className="text-2xl font-bold text-emerald-800">Scan Tanaman</h1>
<h1 className="text-2xl font-bold text-emerald-800">Pindai Daun</h1>
<p className="text-gray-500 mt-1 text-md">
Unggah foto daun jagung untuk dianalisis oleh sistem AI kami secara
real-time.
real-time
</p>
</div>
<Button asChild variant="outline">
@@ -122,8 +119,8 @@ export function ScanPage() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column: Upload Area */}
<div className="lg:col-span-2 space-y-4">
<Card className="w-full lg:h-117 py-3">
<CardContent className="px-6 py-4 h-full flex flex-col">
<Card className="w-full h-fit py-3">
<CardContent className="px-6 py-4 flex flex-col">
<div className="text-black flex items-center gap-2 mb-3 text-lg font-semibold">
<Camera className="text-green-500" size={25} />
Area Unggah Gambar
@@ -170,12 +167,16 @@ export function ScanPage() {
className="w-full border-2 border-dashed border-green-300 rounded-md p-10 h-60 text-center cursor-pointer"
onClick={() => inputRef.current?.click()}
>
<Upload className="mx-auto text-green-500 mb-3" size={48} />
<Upload
className="mx-auto text-green-500 mb-3"
size={48}
/>
<h3 className="font-semibold text-base text-gray-800 mb-1">
Seret & Lepas Foto Daun
</h3>
<p className="text-xs text-gray-500 mb-3">
atau klik untuk memilih file berkas dari perangkat Anda
atau klik untuk memilih file berkas dari perangkat
Anda
</p>
<div className="flex flex-wrap gap-2 justify-center">
<div className="bg-green-100 text-green-700 px-3 py-1 rounded-full inline-flex items-center gap-1 text-xs font-medium">
+3 -4
View File
@@ -286,12 +286,11 @@ export function TelemetryPage() {
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl md:text-[28px] font-extrabold text-[#214B11] flex items-center gap-3">
<Activity className="h-7 w-7 text-[#48A111]" />
<h1 className="text-2xl font-bold text-emerald-800">
Telemetry Dashboard
</h1>
<p className="text-sm text-muted-foreground mt-0.5">
Real-time metrics from Prometheus
<p className="text-gray-500 mt-1 text-md">
Real-time monitoring dari performa sistem dan aplikasi ZeaVis Edu
{error && <span className="text-amber-600 ml-2">(partial {error})</span>}
</p>
</div>
+67 -1
View File
@@ -5,6 +5,7 @@
"": {
"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",
},
@@ -40,6 +41,9 @@
"apps/tauri": {
"name": "@zeavis/tauri",
"version": "0.1.0",
"dependencies": {
"sharp": "0.35.1",
},
"devDependencies": {
"@tauri-apps/cli": "^2",
},
@@ -50,6 +54,8 @@
"dependencies": {
"@radix-ui/react-slot": "^1.2.4",
"@tanstack/react-query": "^5.100.11",
"@tauri-apps/plugin-deep-link": "^2",
"@tauri-apps/plugin-opener": "^2",
"@vitejs/plugin-react": "^6.0.2",
"@zeavis/shared": "workspace:*",
"class-variance-authority": "^0.7.1",
@@ -93,7 +99,7 @@
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
@@ -157,6 +163,60 @@
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="],
"@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-T15JRWOubQ3f5+GxnWeIvo47u5qV0M9HBgJhT+f2gE1e9e6OhR6K73Re52Hm80qWcu1DNb3GweKmpr/MnuP2Ow=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.0" }, "os": "darwin", "cpu": "x64" }, "sha512-t1CPD0cr7XCHjwUj6tQ5MC0pCi866I+gUW6zbUX4aFPnKd1DFBtk0M+gWcjX8VeEzgfCNiSiNTVFZ6b7kvdbnQ=="],
"@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.1", "", { "dependencies": { "@img/sharp-wasm32": "0.35.1" }, "os": "freebsd" }, "sha512-MBSQXqNPThW9EcZ905H6N4sEdX5EwZEYzGx5EBq9ncDCGJALMiY1xPFJxNdzuB1iBjLOpIfxajM6YxdvwmQSLA=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A=="],
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.0", "", { "os": "linux", "cpu": "none" }, "sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.0" }, "os": "linux", "cpu": "arm" }, "sha512-jygmR02PpCYypt7xB7nst1vqjZp/BpRA/Kf9nK7qRponJ/KrLPaZWEG4G15z1d2FZ6XqI+T0350ha3RSnKx24A=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.0" }, "os": "linux", "cpu": "arm64" }, "sha512-ErCRyGU7LeoaFBZ0xW8hhLlXzhAg80sc4vxePB86qvtEvW1jEhhmbiNBP4oEzZfPMnu6HwHXfzD2W2kBU+RnCw=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.0" }, "os": "linux", "cpu": "ppc64" }, "sha512-LUWZ2+r2UoLCd8j0RLCwQ4gL6w47+Y7igxtVnPIDXOOEjV86LpBkAHq5VpJeg+GHbw0KN/JWlPJOdZjyZnFqFQ=="],
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.0" }, "os": "linux", "cpu": "none" }, "sha512-i7x6J3mwF4JgT0sM4V4WlAWdJ0bucPtA9rzO1bTji1n5qgBq/W5nn87RvOQPleuuxahNoLdTngByD8/vDDLArw=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.0" }, "os": "linux", "cpu": "s390x" }, "sha512-0zSaTUjTF0kIWTSYxD4EG/nvCU4jez53+3RdURtoY3HvbXtIQ98W90JnrGz/oLRFuEnfIy9+7xeq883euc0ZWw=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.0" }, "os": "linux", "cpu": "x64" }, "sha512-NbJD4mWdeyrNQKluO/tR/wBDOelcowSVGNBWxI0e3ZtlXc6F/UOVKDj1MLD4zl3oHTuvKW3s+MA9N54YTldAYw=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.0" }, "os": "linux", "cpu": "arm64" }, "sha512-VoW2sQCWI+0YIKQEmWJ8vzaQjTg9wIyfkFpvEfAS2h43X6iHu7GTk1hhOgB4IpSzCHe8UwQZIcx7b81VTaOrJA=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.1", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.0" }, "os": "linux", "cpu": "x64" }, "sha512-LjBoSd/c5JU0/K5MwzDMlgsSRP2bPn98JQGFFQAOLQ0bU/1z4ekxUdSKY9BmlwSh/cA+OrvpgsWqfZyYfVHBRw=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.1", "", { "dependencies": { "@emnapi/runtime": "^1.11.0" } }, "sha512-PCQUoQdZyE8tp3HpbevuihfUmgSP4qWI0FGEPWoeXqaS+cUrFfemabHQiebUmUmlUhCuNnQMxGrQ+CPqK4hnxg=="],
"@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.1", "", { "dependencies": { "@img/sharp-wasm32": "0.35.1" }, "cpu": "none" }, "sha512-xU2ml2bU2OPxYVvW2A6ae4M1g5QKyhKG06P4FAt+YEaFQQO0919Qx+XxIZEUuWTMoDViLpMws2/dQwoe/VcA6A=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-IkmHwuFhYpd3bTsN5SAahjwhiAcyXPooBt8vEUgxY3T0IP70sSJ0nU1xiPzZY8AH/OB1XpV3j8aZSVSOSfTbdA=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-wQahqCi9MD8Yxzg4gVM4fNrZxh+r6vD55PyIg+WJPaM5ZRUyF35iQpwJCuma3r6viU9/8Pxlc+XHV+woVa6nCQ=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.1", "", { "os": "win32", "cpu": "x64" }, "sha512-WzBtkYtZHATLPe8XRharxZXxQ9cdLrQWHiwxt+BJ5rBsisQrKeeV86ErxPSVhcG6xCEuNhs0SqLpWr7XDa2k6w=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -615,8 +675,12 @@
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@7.8.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA=="],
"set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
"sharp": ["sharp@0.35.1", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.1", "@img/sharp-darwin-x64": "0.35.1", "@img/sharp-freebsd-wasm32": "0.35.1", "@img/sharp-libvips-darwin-arm64": "1.3.0", "@img/sharp-libvips-darwin-x64": "1.3.0", "@img/sharp-libvips-linux-arm": "1.3.0", "@img/sharp-libvips-linux-arm64": "1.3.0", "@img/sharp-libvips-linux-ppc64": "1.3.0", "@img/sharp-libvips-linux-riscv64": "1.3.0", "@img/sharp-libvips-linux-s390x": "1.3.0", "@img/sharp-libvips-linux-x64": "1.3.0", "@img/sharp-libvips-linuxmusl-arm64": "1.3.0", "@img/sharp-libvips-linuxmusl-x64": "1.3.0", "@img/sharp-linux-arm": "0.35.1", "@img/sharp-linux-arm64": "0.35.1", "@img/sharp-linux-ppc64": "0.35.1", "@img/sharp-linux-riscv64": "0.35.1", "@img/sharp-linux-s390x": "0.35.1", "@img/sharp-linux-x64": "0.35.1", "@img/sharp-linuxmusl-arm64": "0.35.1", "@img/sharp-linuxmusl-x64": "0.35.1", "@img/sharp-webcontainers-wasm32": "0.35.1", "@img/sharp-win32-arm64": "0.35.1", "@img/sharp-win32-ia32": "0.35.1", "@img/sharp-win32-x64": "0.35.1" } }, "sha512-lW979AMi+ESidzMv/Lnv+F9bknzLyxLqFI05Sm433vOeRcltgxQmXpnfOOFIAlKtwXU/ksupm2srQoFCkR214g=="],
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
@@ -681,6 +745,8 @@
"@reduxjs/toolkit/immer": ["immer@11.1.8", "", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="],
"@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
+6
View File
@@ -31,6 +31,8 @@ services:
networks:
- app-shared-net
- telemetry-net
ports:
- "${TS_IP:-0.0.0.0}:3000:3000"
env_file:
- .env
environment:
@@ -51,6 +53,8 @@ services:
image: prom/node-exporter:v1.8.2
container_name: zeavis-node-exporter
restart: unless-stopped
ports:
- "${TS_IP:-0.0.0.0}:9100:9100"
command:
- "--path.rootfs=/host"
- "--web.listen-address=:9100"
@@ -68,6 +72,8 @@ services:
networks:
- app-shared-net
- telemetry-net
ports:
- "${TS_IP:-0.0.0.0}:8000:8000"
env_file:
- .env
environment:
+1
View File
@@ -15,6 +15,7 @@
"packages/*"
],
"dependencies": {
"@tauri-apps/api": "2.11.0",
"@tauri-apps/plugin-deep-link": "2.4.9",
"@tauri-apps/plugin-opener": "2.5.4"
}