Backend entity has no type column yet; whitelist the type field in DTO
so forbidNonWhitelisted doesn't reject requests from the mobile app.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users registered before referral-service was deployed have no profile row.
getMyInfo() now auto-provisions a profile on first access instead of
throwing NotFoundException.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- HeartbeatService: add NativeAdapter on Android to fix DNS via Java stack;
fix status check from == 200 to >= 200 && < 300
- TelemetryUploader: same two fixes (refactor to _buildDio static method)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Android DNS fix (native_dio_adapter):
- Dart's getaddrinfo() fails on some Android 12+ devices when browser works
fine — root cause is Dart native socket using different DNS path than
Android Java stack; fix by using NativeAdapter (OkHttp) on Android
- Add native_dio_adapter ^1.5.1 to pubspec; enable in ApiClient for Android
Message page bugs (5 fixes):
- Fix offset->page: Flutter sent ?offset=0 but backend NotificationQueryDto
uses page/limit (1-based); now sends ?page=1&limit=50
- Fix announcement tab API: Tab 2 now calls /api/v1/announcements (separate
resource) instead of /api/v1/notifications?type=ANNOUNCEMENT
- Fix markAllAsRead routing: calls correct API based on active tab
- Fix markAsRead routing: announcement items use announcements API
- Fix detail navigation: passes NotificationItem as route argument
Backend notification-service:
- Add PUT /api/v1/notifications/read-all endpoint
- Add markAllReadByUserId() to repository interface + TypeORM implementation
- Add markAllRead() to NotificationService
i18n (4 languages): add time.justNow/minutesAgo/hoursAgo/daysAgo keys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: TelemetryService.initialize() is async (device info collection,
SharedPreferences I/O, remote config sync), taking several hundred ms to
complete. RouteObserver page_view events and auth events fire synchronously
during the initial navigation before initialization finishes, causing the
"TelemetryService not initialized, event ignored" drops seen in logs.
Fix: add _preInitBuffer (max 50 events). logEvent() now enqueues events
instead of dropping when _isInitialized=false. After initialization
completes, all buffered events are replayed in order via logEvent(),
ensuring no startup events are lost.
The network errors ("Failed host lookup: api.gogenex.com") are unrelated —
they are expected in offline test environments and handled gracefully.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SharePage: QR code now encodes actual APK download URL from admin-service
(GET /api/v1/app/version/download/{id} via Kong), referral code shown
separately below QR for manual entry on registration
- Add referral reward marketing section: direct bonus / team levels (50
layers) / new user welcome package — drives viral growth
- Show referrer info card when current user was invited via referral code
- Share text updated to marketing copy with APK link + referral code
- VersionChecker.getLatestApkUrl(): fetches latest APK URL regardless of
needUpdate (passes version=0.0.0 to force server response)
- UpdateService.getLatestApkUrl(): exposes APK URL fetch for UI layer
- i18n (zh-CN/zh-TW/en/ja): add 14 new keys for marketing content,
APK download hints, reward program descriptions, and referrer info
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
context.t() (Localizations) cannot be called during initState() — inherited
widgets are not yet mounted. When download fails instantly (e.g. no network),
setState with context.t() inside _startDownload fired synchronously within
initState, causing:
dependOnInheritedWidgetOfExactType called before initState() completed
Fix: add _downloadStarted guard flag; call _startDownload() from
didChangeDependencies() instead of initState(), where context is fully ready.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add connectivity_plus: ^6.0.3 to pubspec.yaml
- DeviceInfoCollector._getNetworkType() calls Connectivity().checkConnectivity()
and maps ConnectivityResult → wifi / mobile / ethernet / vpn / bluetooth / none / unknown
- networkType field in DeviceContext is now populated on every cold start
- All page_view and session events carry the accurate network type in device props
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Mix in WidgetsBindingObserver to detect foreground/background transitions
- On resumed: run a silent version check (no dialog) and only show the
update dialog if a new version is actually available
- Throttle resume checks to once per 2 minutes to avoid excessive API calls
- Once the update dialog has been shown, skip further checks for the rest
of the session; user won't be re-prompted until next cold start
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ParsePackageUseCase.execute now accepts onProgress callback
- RegisterVersionUseCase added for the new /register endpoint
- use-upload.ts now imports only from application layer (no direct infra import)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the flow uploaded the 53MB file twice:
1. POST /parse → parse metadata (file discarded)
2. POST /upload → parse again + save (file sent again)
New flow — file sent exactly once:
1. POST /parse → upload file, save to disk, parse metadata
returns {versionName, versionCode, minSdkVersion, storageKey, fileSize, fileSha256}
2. POST /register → JSON only (no file), creates DB record using storageKey
Frontend:
- handleFileChange: async, immediately uploads to /parse with progress bar (0-100%)
- handleSubmit: calls /register with storageKey + form metadata (instant)
- Upload modal: real-time progress bar, "confirm" button disabled until parse complete
- Console logs at every step for debugging
Backend:
- POST /parse: saves file after parsing, returns storageKey in response
- POST /register: new endpoint, accepts JSON + storageKey, creates version record
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
refreshAccessToken() was discarding the new refresh token returned by
/auth/refresh, reusing the old (now-invalidated) one on next expiry.
This caused the second refresh to return 401, kicking the user to login
after just 15 minutes (two access token lifetimes).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously any refresh failure (network error, service restart, timeout)
would clear localStorage and redirect to /login — kicking active users.
Now only a deliberate token rejection (HTTP 401/403) causes logout.
Transient errors are rejected silently without destroying the session.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Logs file size on receipt, parse duration, file save duration, and
total request time — to pinpoint where upload latency comes from.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
File select now auto-parses the APK/IPA (sends to /parse endpoint),
auto-fills versionName/buildNumber/minOsVersion, and keeps the upload
button disabled (isParsing=true) until parsing finishes — matching
RWADurian's proven UX exactly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
File was being uploaded twice:
1. POST /parse on file select (for form auto-fill)
2. POST /upload on submit
Remove the parse network call entirely. Server already parses the APK/IPA
buffer as part of the upload handler. User fills form fields manually.
Single upload, single public-internet transfer.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the APK/IPA upload required two full public-internet transfers:
1. POST /parse → browser → gateway → admin-service (full file, for metadata)
2. PUT presigned → browser → oss.gogenex.com / MinIO (full file, to store)
Now follows the same pattern as RWADurian admin-service:
- Single multipart POST /admin/versions/upload
- admin-service parses buffer in-memory (yauzl / unzipper)
- Saves to local disk (UPLOAD_DIR env, default ./uploads)
- Download served via existing GET /app/version/download/:id (streams local file)
Changes:
- file-storage.service.ts: drop minio dep, use fs/promises + crypto
- admin-version.controller.ts: POST upload now accepts multipart file,
removes GET presigned-url endpoint (no longer needed)
- version.repository.ts (frontend): single FormData POST, removes
three-step presigned-URL flow
Result: file crosses public internet once instead of twice.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Kong container uses network_mode:host so it shares the gateway's
network namespace and can reach 192.168.1.222:PORT directly.
Listen on 127.0.0.1:48080 (local only, Nginx proxies externally).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- infrastructure/kong/: Kong declarative config for gateway server
All service URLs use http://192.168.1.222:PORT (internal server)
admin-service gets extended timeouts (300s) for large uploads
- docker-compose.yml: admin-service uses MINIO_ENDPOINT=192.168.1.200:9200
Plain HTTP via Nginx internal proxy (no SSL, no extra_hosts needed)
New upload path:
Browser → Nginx:443 → Kong:48080 (local) → admin-service(LAN) → MinIO:9200(local)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Container maps oss.gogenex.com → 192.168.1.200 (LAN IP) so it
connects to Nginx:443 which proxies to localhost:9100 (MinIO).
Port 443 is already open in UFW; avoids hairpin NAT and raw iptables
drop rules that block direct access to 192.168.1.200:9100.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
admin-service runs on 192.168.1.222 (LAN). Connecting to MinIO via
public IP 154.84.135.121 fails with ETIMEDOUT (hairpin NAT). Use
internal gateway IP 192.168.1.200:9100 (no SSL) for S3 API calls.
Public download URLs still use https://oss.gogenex.com.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause was proxy_request_buffering off in Nginx gateway (already removed).
Kong should use default settings, matching IT0 reference architecture.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add KONG_NGINX_PROXY_PROXY_REQUEST_BUFFERING=off so Kong streams
the request body to admin-service without buffering to disk.
Root cause: Nginx streams (proxy_request_buffering off) → Kong buffers
to disk → Kong returns 400 with empty body when forwarding to upstream.
Fix: Kong also streams, matching Nginx's streaming behavior end-to-end.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend wraps data in extra layer:
system-health → {code:0, data:{services:[...]}}
realtime-trades → {code:0, data:{items:[...], total:N}}
HttpClient strips outer data but leaves inner object.
Fix: type as {services/items} and access nested arrays.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- auth.store: eslint-disable with explicit comment for intentional infra access
(session orchestration is a designated cross-layer boundary)
- Add auth.use-cases.ts (LoginUseCase / LogoutUseCase) for use by views/hooks
- Fix no-explicit-any in AppVersionManagementPage (use unknown + type assertion)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>