Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
153abf4352 | ||
|
|
da5c7c1cfa | ||
|
|
58d4cc0164 | ||
|
|
a3105200e3 | ||
|
|
c75cba214e | ||
|
|
c010d16aaa | ||
|
|
94ed062026 |
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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/.
|
||||
# =============================================================================
|
||||
|
||||
Generated
+616
@@ -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",
|
||||
]
|
||||
|
||||
+133
-98
@@ -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,10 +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 } from "@/lib/tauri";
|
||||
import { setupDeepLinkHandler, consumeDeepLinkTarget } from "@/lib/tauri";
|
||||
|
||||
function LogoutProses() {
|
||||
const setUser = useAuthStore((state) => state.setUser);
|
||||
@@ -32,7 +34,7 @@ function LogoutProses() {
|
||||
|
||||
}).catch((error) => {
|
||||
console.error("Oops, gagal logout dari server:", error);
|
||||
setUser(null);
|
||||
setUser(null);
|
||||
});
|
||||
}, [setUser]);
|
||||
|
||||
@@ -43,105 +45,138 @@ import { trackPageView, trackError } from "./lib/telemetry";
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{ path: "/", element: <Navigate to="/login" replace /> },
|
||||
{ path: "/login", element: <LoginPage /> },
|
||||
{ path: "/register", element: <RegisterPage /> },
|
||||
{
|
||||
path: "/dashboard",
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<MainLayout>
|
||||
<DashboardPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/scan",
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<MainLayout>
|
||||
<ScanPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/library",
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<MainLayout>
|
||||
<LibraryPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/diagnoses",
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<MainLayout>
|
||||
<DiagnosesPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/diagnoses/:id",
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<MainLayout>
|
||||
<DiagnosisDetailPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/expert/reviews",
|
||||
element: (
|
||||
<AuthGuard requireExpert={true}>
|
||||
<MainLayout>
|
||||
<ExpertReviewsPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/catalog",
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<MainLayout>
|
||||
<CatalogPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/catalog/:slug",
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<MainLayout>
|
||||
<DiseaseDetailPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/logout",
|
||||
element: (
|
||||
<LogoutProses />
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/telemetry",
|
||||
element: (
|
||||
<MainLayout>
|
||||
<TelemetryPage />
|
||||
</MainLayout>
|
||||
),
|
||||
element: <DeepLinkRouterHandler />,
|
||||
children: [
|
||||
{ path: "/", element: <Navigate to="/login" replace /> },
|
||||
{ path: "/login", element: <LoginPage /> },
|
||||
{ path: "/register", element: <RegisterPage /> },
|
||||
{
|
||||
path: "/dashboard",
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<MainLayout>
|
||||
<DashboardPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/scan",
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<MainLayout>
|
||||
<ScanPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/library",
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<MainLayout>
|
||||
<LibraryPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/diagnoses",
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<MainLayout>
|
||||
<DiagnosesPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/diagnoses/:id",
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<MainLayout>
|
||||
<DiagnosisDetailPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/expert/reviews",
|
||||
element: (
|
||||
<AuthGuard requireExpert={true}>
|
||||
<MainLayout>
|
||||
<ExpertReviewsPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/catalog",
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<MainLayout>
|
||||
<CatalogPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/catalog/:slug",
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<MainLayout>
|
||||
<DiseaseDetailPage />
|
||||
</MainLayout>
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "/logout",
|
||||
element: <LogoutProses />,
|
||||
},
|
||||
{
|
||||
path: "/telemetry",
|
||||
element: (
|
||||
<MainLayout>
|
||||
<TelemetryPage />
|
||||
</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(() => {
|
||||
|
||||
@@ -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,10 +30,7 @@ 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);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
+55
-20
@@ -1,6 +1,13 @@
|
||||
/**
|
||||
* Lightweight Tauri environment detection and utilities.
|
||||
* 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;
|
||||
@@ -22,7 +29,6 @@ function tauriInvoke(): (cmd: string, args?: Record<string, unknown>) => Promise
|
||||
|
||||
export async function openUrl(url: string): Promise<void> {
|
||||
if (!isTauri()) {
|
||||
// Not in Tauri — normal browser navigation
|
||||
window.location.href = url;
|
||||
return;
|
||||
}
|
||||
@@ -31,47 +37,76 @@ export async function openUrl(url: string): Promise<void> {
|
||||
await invoke('plugin:opener|open_url', { url });
|
||||
} catch (err) {
|
||||
console.error('Tauri openUrl failed, trying fallback:', err);
|
||||
// Fallback: navigate the WebView (Google will block, but best effort)
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
function processDeepLinkUrl(url: string): void {
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const target = u.pathname + u.search + u.hash;
|
||||
if (target && target !== '/') {
|
||||
window.location.href = target;
|
||||
return;
|
||||
}
|
||||
} catch { /* fall through */ }
|
||||
// ── Deep link handling (no full reload) ─────────────────────────────────
|
||||
|
||||
// Fallback: handle both :// and :/ custom schemes
|
||||
let match = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
|
||||
if (!match) match = url.match(/^[^:]+:\/(\/.*)?$/);
|
||||
if (match?.[1]) window.location.href = match[1];
|
||||
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 just opened via intent:// or custom scheme
|
||||
// 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]) processDeepLinkUrl(urls[0]);
|
||||
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(() => { /* plugin may not be registered yet */ });
|
||||
.catch(() => {});
|
||||
|
||||
// Warm-start: listen for new URLs while app is running
|
||||
// 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) processDeepLinkUrl(url);
|
||||
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] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -33,6 +33,7 @@ export function ScanPage() {
|
||||
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
// Camera mode state
|
||||
const [useCamera, setUseCamera] = useState(false);
|
||||
@@ -119,8 +120,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
|
||||
@@ -164,8 +165,25 @@ export function ScanPage() {
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
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 rounded-md p-10 h-60 text-center cursor-pointer transition-colors duration-200 ${
|
||||
isDragging
|
||||
? "border-blue-500 bg-blue-100"
|
||||
: "border-green-300"
|
||||
}`}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
}}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const droppedFile = e.dataTransfer.files?.[0];
|
||||
if (droppedFile) {
|
||||
handleFile(droppedFile);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Upload
|
||||
className="mx-auto text-green-500 mb-3"
|
||||
|
||||
@@ -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
-1
Submodule telemetry updated: 2582a53592...89d560baf8
Reference in New Issue
Block a user