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>
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>
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>
Background: Google blocks OAuth in embedded WebView (403 disallowed_useragent).
Solution: open Google login in the Android system browser, then deep-link
back to the Tauri app via custom scheme after callback.
Changes:
- Tauri: add tauri-plugin-opener + tauri-plugin-deep-link to Cargo.toml
- Tauri: register plugins in lib.rs, add capabilities
- Web: auth-form.tsx Google button uses openUrl() via @tauri-apps/plugin-opener
on Tauri (opens in system browser), falls back to window.location.href
- Web: add lib/tauri.ts for isTauri() detection + lazy opens
- API: /auth/google accepts ?platform=tauri → encodes into OAuth state param
- API: /auth/google/callback decodes state → if tauri, renders HTML page
that deep-links back via zeavisedu:// scheme; if web, 302 redirect
- Android: patch script adds deep link intent filter for zeavisedu:// scheme
Co-Authored-By: Claude <noreply@anthropic.com>
Elysia's set.redirect returns 200 OK instead of 302 redirect on the
current version. Use set.status = 302 + set.headers['Location'] instead
for both /auth/google (Google OAuth redirect) and /auth/google/callback
(all redirect paths: errors, success token delivery).
Co-Authored-By: Claude <noreply@anthropic.com>
API (apps/api/src/routes/auth.ts):
- Exchange authorization code for tokens via Google's token endpoint
- Decode id_token payload (JWT) to extract sub, email, name
- Find user by googleId → fall back to email match (link accounts)
- Auto-create user if neither found (role: 'user', no password)
- Create session + set cookie, redirect to /login?token=<token>
Web (apps/web/src/pages/login-page.tsx):
- Consume ?token= query param from OAuth redirect
- Store token in localStorage for future API calls
- Fetch /auth/me to hydrate Zustand store, then navigate to dashboard
- Show OAuth errors from ?error= query param
Co-Authored-By: Claude <noreply@anthropic.com>
Android WebView blocks third-party cookies by default. This patch:
- Returns session token in login/register response body
- Stores token in localStorage, sends via Authorization: Bearer header
- Backend getCurrentUser supports Authorization header fallback
Co-Authored-By: Claude <noreply@anthropic.com>
- Cookie SameSite now dynamic: None;Secure when behind HTTPS proxy, Lax otherwise
- AuthGuard useEffect no longer overwrites Zustand store with null from background refetch
- AuthInitializer: add staleTime 30s
Co-Authored-By: Claude <noreply@anthropic.com>
- Set VITE_API_BASE_URL default to production API URL in api-client.ts
so fetch() uses absolute URLs instead of relative paths that fail on Tauri
- Add tauri://localhost and https://tauri.localhost to API CORS allowed origins
- Also fix .env WEB_APP_URL from stale .tech to .my.id
Co-Authored-By: Claude <noreply@anthropic.com>
- Add environment-aware CORS and secure cookie support:
* Add secureCookies config to env.ts based on SECURE_COOKIES env var or https detection
* Integrate @elysiajs/cors with credentials and origin configuration
* Update cookie helpers to use SameSite=None; Secure in production
- Wrap expert review update and insert in database transaction for atomicity:
* Ensures diagnosis status update and review insert succeed together
* Rolls back both operations if either fails
* Preserves behavior: only update if status is needs_review, return badRequest if no row updated
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
In POST /reviews/:diagnosisId, perform the conditional diagnoses update
first with .returning() to determine whether a row was updated. The update
WHERE includes both id and status 'needs_review'. If no row is returned,
return badRequest and do not insert an expert review. Only after a
successful update insert expertReviews. This prevents concurrent reviewers
from both inserting review rows when only one update should win.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Reject diagnoses not in needs_review status with badRequest
- Make update conditional on both id and status needs_review using and()
- Simplify isDiagnosisRecordOrNull type guard with type alias
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Replace `function toReview(row: any)` with typed ReviewRow interface
- Reconstruct DiseaseCatalogItem explicitly from selected fields instead of casting
- Replace `.filter(Boolean)` with typed isDiagnosisRecord predicate
- Add proper type narrowing for verdict and correctedDiseaseSlug in toReview
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace unsafe `as any` casts with proper type guards using `isDiseaseSlug()`
and explicit type casting to `DiagnosisStatus` and `DiseaseCatalogItem`.
Replace non-null assertions with explicit null checks that return
`serviceUnavailable()` errors when database operations fail.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implement diagnosis persistence routes with image upload, classification,
and prediction storage. Add missing HTTP error helpers (unauthorized,
forbidden) and update uploader client to use configurable base URL.
- Create POST /api/v1/diagnoses to upload images and store classifications
- Create GET /api/v1/diagnoses to list user's diagnoses (30 most recent)
- Create GET /api/v1/diagnoses/:id to retrieve diagnosis with predictions
- Add loadDiagnosisRecord helper for consistent diagnosis data loading
- Update uploader-client.ts to use env.uploaderBaseUrl
- Add unauthorized and forbidden error helpers to http-errors.ts
- Register diagnosisRoutes in main app
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Extend shared types for image classification, including PredictionProbability, UploaderMetadata, and ImageClassificationRecord.
- Create image_classifications table in the database with necessary fields and foreign key constraints.
- Implement disease mappers to convert database rows to shared disease records.
- Develop uploader client to handle image uploads to external service.
- Create image model service to load and classify images using TensorFlow.js.
- Add API routes for image classification, including GET for history and POST for new classifications.
- Implement frontend components for image classification form and display results.
- Update dashboard to integrate image classification functionality and display results.
- Document implementation plan for backend image classification.
- Add disease detail page component with data fetching and error handling
- Create shared types for diseases and classifications
- Implement API routes for diseases, classifications, and dashboard summary
- Develop reusable components for risk badge and disease card
- Build catalog page with search and filter functionality
- Update dashboard page with data-backed summary and manual classification form
- Register new routes in the web application
- Ensure type safety and consistency across shared modules