Compare commits

...
18 Commits
Author SHA1 Message Date
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
21 changed files with 422 additions and 109 deletions
+5 -1
View File
@@ -136,7 +136,11 @@ jobs:
rm -rf gen/android rm -rf gen/android
bun tauri android init 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 working-directory: apps/tauri
run: bash scripts/patch-android-manifest.sh run: bash scripts/patch-android-manifest.sh
+58 -9
View File
@@ -4,7 +4,7 @@
<h1 align="center">ZeaVis Edu</h1> <h1 align="center">ZeaVis Edu</h1>
<p align="center"> <p align="center">
<strong>Asisten Edukasi Interaktif untuk Deteksi Penyakit Daun Jagung</strong><br> <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>
</p> </p>
@@ -16,6 +16,7 @@
<a href="#-jadwal"><b>Jadwal</b></a> &bull; <a href="#-jadwal"><b>Jadwal</b></a> &bull;
<a href="#-tech-stack"><b>Tech Stack</b></a> &bull; <a href="#-tech-stack"><b>Tech Stack</b></a> &bull;
<a href="#-memulai"><b>Memulai</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> <a href="#-dokumentasi"><b>Dokumentasi</b></a>
</p> </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 | | Klasifikasi 3 penyakit + 1 daun sehat | Penyakit pada batang atau buah jagung |
| Deteksi berbasis unggah gambar daun | Prediksi tanpa input gambar | | Deteksi berbasis unggah gambar daun | Prediksi tanpa input gambar |
| Rekomendasi obat & penanganan | Diagnosis pengganti ahli/POPT | | Rekomendasi obat & penanganan | Diagnosis pengganti ahli/POPT |
| Aplikasi web edukatif | Aplikasi mobile native | | Aplikasi Web + Android (Tauri 2) | Aplikasi iOS |
### 4 Kelas yang Diklasifikasikan ### 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 | | 2 | **Model ML** | Model Computer Vision terlatih di Google Colab, siap produksi |
| 3 | **UI Antarmuka** | Front-End berbasis React + Vite dengan fitur unggah gambar | | 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 | | 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/ ├── apps/
│ ├── api/ # Backend Elysia/Bun + Drizzle ORM + PostgreSQL │ ├── 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 │ └── web/ # Frontend React + Vite + Tailwind CSS
├── Machine_Learning/ # Pipeline dataset, training Colab, ekspor model ├── Machine_Learning/ # Pipeline dataset, training Colab, ekspor model
│ └── README.md # ⤷ Panduan lengkap pipeline ML │ └── README.md # ⤷ Panduan lengkap pipeline ML
@@ -147,8 +149,9 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
| Komponen | Teknologi | Dokumentasi | | Komponen | Teknologi | Dokumentasi |
|---|---|---| |---|---|---|
| Web Frontend | React, Vite, Tailwind, Zustand, TanStack Query | `apps/web/` | | 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/` | | 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) | | 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) | | Infrastruktur | Docker, Coolify, Traefik, Tailscale | [`infra/README.md`](infra/README.md) |
| Telemetry | Prometheus, ClickHouse, Vector, Vue 3 | `telemetry/` | | Telemetry | Prometheus, ClickHouse, Vector, Vue 3 | `telemetry/` |
@@ -157,15 +160,18 @@ ZeaVis Edu menggunakan **Computer Vision** sebagai asisten edukasi interaktif:
## 🛠️ Tech Stack ## 🛠️ Tech Stack
### Frontend ### Frontend & Mobile
React &bull; Vite &bull; TypeScript &bull; React Router &bull; TanStack Query &bull; Zustand &bull; Tailwind CSS 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 ### Backend API
Bun &bull; Elysia &bull; Drizzle ORM &bull; PostgreSQL &bull; prom-client Bun &bull; Elysia &bull; Drizzle ORM &bull; PostgreSQL &bull; prom-client
### Machine Learning ### Machine Learning
Python &bull; TensorFlow/Keras &bull; EfficientNetV2B0 &bull; Google Colab (GPU T4) 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 ### DevOps & Infrastruktur
Docker &bull; Docker Compose &bull; Coolify &bull; Traefik &bull; Tailscale &bull; GitHub Actions (CI/CD) 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 - **Bun** — runtime & package manager
- **Python 3.93.11** — pipeline ML - **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 - **Docker & Docker Compose** — deployment & telemetry
- **PostgreSQL** — backend API - **PostgreSQL** — backend API
@@ -199,7 +206,9 @@ bun install
bun run dev # Semua service (web + api) bun run dev # Semua service (web + api)
cd apps/web && bun run dev # Hanya frontend cd apps/web && bun run dev # Hanya frontend
cd apps/api && bun run start # Hanya backend API 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 ### 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 ## 📚 Dokumentasi
| Dokumen | Isi | | 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 * Render a page for the Android system browser that uses Chrome's native
* Tauri app via a custom scheme (zeavisedu://). The app's AndroidManifest * `intent://` protocol to open the Tauri app with the session URL.
* must register an intent filter for this scheme. * Falls back to a clickable button if the intent is blocked.
*/ */
function renderTauriDeepLinkPage(targetUrl: string): Response { function renderTauriDeepLinkPage(targetUrl: string): Response {
// Rewrite https://... to zeavisedu://... for the custom scheme // Extract the path + query from the full URL for the intent
const deepLink = targetUrl.replace(/^https?:\/\//, 'zeavisedu://'); 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> const html = `<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Kembali ke ZeaVis Edu</title></head> <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"> <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"> <div style="text-align:center;padding:2rem;max-width:360px">
<p style="color:#166534;font-size:1.1rem;margin-bottom:1.5rem">Login berhasil!<br>Kembali ke aplikasi...</p> <p style="color:#166534;font-size:1.1rem;margin-bottom:1.5rem">Login Google 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> <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;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> <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> </div>
<script>window.location.href=${JSON.stringify(deepLink)};</script> <script>
// Auto-open the intent
window.location.href = ${JSON.stringify(intentUrl)};
</script>
</body></html>`; </body></html>`;
return new Response(html, { return new Response(html, {
status: 200, status: 200,
@@ -297,12 +314,18 @@ 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, request.headers); const sessionCookie = createSessionCookie(token, request.headers);
authCounter.labels('login', 'true').inc(); authCounter.labels('login', 'true').inc();
const successUrl = `${env.webAppUrl}/login?token=${encodeURIComponent(token)}`; 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.status = 302;
set.headers['Location'] = successUrl; set.headers['Location'] = successUrl;
} catch (err) { } catch (err) {
+3
View File
@@ -12,5 +12,8 @@
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2" "@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);
});
+3 -3
View File
@@ -4,10 +4,10 @@
"version": "0.1.0", "version": "0.1.0",
"identifier": "com.zeavis.edu", "identifier": "com.zeavis.edu",
"build": { "build": {
"frontendDist": "../web/dist", "frontendDist": "../web/dist-tauri",
"devUrl": "http://localhost:5173", "devUrl": "http://localhost:5173",
"beforeBuildCommand": "cd ../web && bun run build", "beforeBuildCommand": "echo 'Nothing to build (APK loads live web content)'",
"beforeDevCommand": "cd ../web && bun run dev" "beforeDevCommand": "echo 'Dev mode - connect to localhost:5173'"
}, },
"app": { "app": {
"withGlobalTauri": false, "withGlobalTauri": false,
+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": { "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",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

+5 -5
View File
@@ -37,12 +37,12 @@ export function AuthForm({ mode, isSubmitting, error, googleOAuthEnabled, onSubm
}, []); }, []);
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>
+31 -16
View File
@@ -29,27 +29,42 @@ export async function openUrl(url: string): Promise<void> {
* callback page redirects to zeavisedu://... which triggers this listener. * callback page redirects to zeavisedu://... which triggers this listener.
* We extract the path + query and navigate the WebView there. * We extract the path + query and navigate the WebView there.
*/ */
function processDeepLinkUrl(url: string): void {
// url looks like: zeavisedu://login/login?token=xxx
// We need to extract path+query and navigate the WebView there
try {
const u = new URL(url);
const target = u.pathname + u.search + u.hash;
if (target && target !== '/') {
window.location.href = target;
return;
}
} catch { /* try fallback below */ }
// Fallback: handle both double-slash (://) and single-slash (:/) schemes
let match = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
if (!match) {
match = url.match(/^[^:]+:\/(\/.*)?$/);
}
if (match?.[1]) {
window.location.href = match[1];
}
}
export async function setupDeepLinkHandler(): Promise<void> { export async function setupDeepLinkHandler(): Promise<void> {
if (!isTauri()) return; if (!isTauri()) return;
const { onOpenUrl } = await import('@tauri-apps/plugin-deep-link'); const { onOpenUrl, getCurrent } = await import('@tauri-apps/plugin-deep-link');
// Handle cold-start deep links (app opened via intent)
getCurrent().then((urls) => {
if (urls?.[0]) processDeepLinkUrl(urls[0]);
}).catch(() => { /* ignore */ });
// Handle warm-start deep links (app already running, new intent received)
onOpenUrl((urls) => { onOpenUrl((urls) => {
for (const url of urls) { for (const url of urls) {
// url looks like: zeavisedu://zeavisedu.asepharyana.my.id/login?token=xxx processDeepLinkUrl(url);
// Extract path + query after the host
try {
const u = new URL(url);
const target = u.pathname + u.search + u.hash;
if (target && target !== '/') {
window.location.href = target;
}
} catch {
// If URL parsing fails, try to extract everything after the scheme
const match = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
if (match?.[1]) {
window.location.href = match[1];
}
}
} }
}); });
} }
+3 -3
View File
@@ -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>
+43 -29
View File
@@ -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.
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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
+23 -5
View File
@@ -65,15 +65,29 @@ export function LoginPage() {
<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="flex flex-col items-center gap-3"> <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" /> <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> </div>
</main> </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}
@@ -87,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>
+12 -1
View File
@@ -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}
+19 -18
View File
@@ -36,21 +36,18 @@ export function ScanPage() {
// Camera mode state // Camera mode state
const [useCamera, setUseCamera] = useState(false); const [useCamera, setUseCamera] = useState(false);
const handleCameraCapture = useCallback( const handleCameraCapture = useCallback((file: File) => {
(file: File) => { setFileName(file.name);
setFileName(file.name); const url = URL.createObjectURL(file);
const url = URL.createObjectURL(file); setPreviewUrl(url);
setPreviewUrl(url);
const img = new Image(); const img = new Image();
img.onload = () => { img.onload = () => {
setImageDimensions({ width: img.width, height: img.height }); setImageDimensions({ width: img.width, height: img.height });
}; };
img.src = url; img.src = url;
setUseCamera(false); setUseCamera(false);
}, }, []);
[],
);
const mutation = useMutation({ const mutation = useMutation({
mutationFn: (file: File) => apiClient.createDiagnosis(file), mutationFn: (file: File) => apiClient.createDiagnosis(file),
@@ -107,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">
@@ -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" className="w-full border-2 border-dashed border-green-300 rounded-md p-10 h-60 text-center cursor-pointer"
onClick={() => inputRef.current?.click()} 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"> <h3 className="font-semibold text-base text-gray-800 mb-1">
Seret & Lepas Foto Daun Seret & Lepas Foto Daun
</h3> </h3>
<p className="text-xs text-gray-500 mb-3"> <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> </p>
<div className="flex flex-wrap gap-2 justify-center"> <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"> <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 */} {/* 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>
+66 -1
View File
@@ -40,6 +40,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",
}, },
@@ -50,6 +53,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",
@@ -93,7 +98,7 @@
"@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], "@emnapi/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=="],
@@ -157,6 +162,60 @@
"@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="], "@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=="],
@@ -615,8 +674,12 @@
"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=="],
@@ -681,6 +744,8 @@
"@reduxjs/toolkit/immer": ["immer@11.1.8", "", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="], "@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/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/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],