- 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>
Instance-level default Content-Type: application/json was overriding
browser's auto-generated multipart/form-data boundary. Remove it for
FormData so browser sets correct Content-Type with boundary.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without boundary multer receives undefined file. Also add guards in
backend parse/upload to avoid crash if file is missing.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Prevent TypeError if useApi returns non-array shape
- Add HttpClient.get logging to trace raw vs unwrapped response
- Parse timeout: 120s → 300s (matches upload, avoids timeout on large files)
- Show hint for large files (>30MB) during parse
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- auth.store: persist refreshToken alongside accessToken
- http.client: on 401, auto-refresh token and retry original request
with mutex lock to prevent concurrent refresh calls; only redirect
to /login if refresh itself fails
- upload modal: restore auto-parse on file select; show warning if
parse fails; add console logs for debugging; fix button disabled
during parsing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove auto-parse on file select (was uploading 48MB twice, took 100+ sec)
- Backend /upload already parses APK internally, version fields are now optional
- Show file name + size after selection
- Show progress hint during upload
- Better error extraction from API response
- Clear error when new file is selected
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Manually setting Content-Type: multipart/form-data without the boundary
causes the server to reject the request. Axios automatically sets the
correct header with boundary when FormData is passed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ManifestParser(buffer) 内部自带 BinaryXmlParser,无需先调用 BinaryXmlParser.parse()
再把结果传入 ManifestParser,否则导致 readUInt16LE is not a function。
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>