Adopt agents-settings kit, ship Product/SKU scan models, harden auth, verify OCR accuracy
Backend (app-pfm-ocr-v2/backend): - Product/SKU scan feature complete: trained DINOv2 index (118 reference photos, 16 SKU classes) and YOLO classifier (83.3% top-1 val accuracy), fixed scripts/install-pipeline.sh (was missing ultralytics/torch), fully browser-verified end-to-end on /scan-pfm. Mobile m-scan-pfm page cancelled (Flutter app handles mobile; web UI is desktop-only for pipeline testing). - Fixed a real data-loss bug: Save Ground Truth (scan-pfm and the DO-flow's manual-label) was silently writing into the pfm-web-app container's ephemeral filesystem instead of the host, because /sources wasn't bind-mounted in docker-compose.yml. Added the mount, recovered an orphaned entry. - accounts.password is now bcrypt-hashed (bcryptjs, idempotent migration in db/init.ts) instead of plaintext; login route compares hashes. - /api/v1/documents/* (list, PUT, upload) now enforces real 401 auth, matching what the Flutter client already sends. The "classic" routes deliberately stay open — they're dev-only web UI with no login flow and won't exist in production. - OCR accuracy investigated end-to-end: real baseline is 95.10% overall (target met; accuracy_report.md was stale at 75.04%, now flagged). Fixed one genuine parser.ts bug (SO/DO field duplication in the global fallback regex); remaining gaps are OCR/layout-model limitations, not parser bugs. - Adopted a standalone copy of the fhanyuh/agents-settings e/n workflow scoped to backend/ (AGENTS.md Part A/B split, SKILLS.md, plans/, docs/), independent of the root copy which now covers Flutter only. - next-implementation.md deleted; content folded into backend/plans/next-enhancements.md for traceability. Root: - Adopted fhanyuh/agents-settings kit (AGENTS.md, SKILLS.md, plans/, docs/feature-list.md), scoped to the Flutter app only. - Pending documents queue now persists to Hive (lib/core/storage) instead of memory-only, surviving an app kill mid-upload. Removed backend_backup/ (stale Express/Prisma prototype, superseded by pfm-web-app) and the completed plans/next-enhancement-plan.md checklist. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3df9f6ec5d
commit
e60ab63154
129 files changed
+8520
-6684
No files matched your search
+51
-33
@@ -5,16 +5,21 @@ import 'package:google_fonts/google_fonts.dart';
|
||||
class AppConfig {
|
||||
// ─── API Configuration ────────────────────────────────────────────────────
|
||||
//
|
||||
// Dual-Mode Configuration:
|
||||
// - If public ngrok is running and active, the app will use it automatically.
|
||||
// - If ngrok is down/inactive or unreachable, it falls back to the LAN/Local URL.
|
||||
// Dual-Mode Configuration, tried in this order at every app startup:
|
||||
// 1. Local/LAN backend (fast, low-latency - the common case when the phone
|
||||
// and backend are on the same Wi-Fi).
|
||||
// 2. Public Ngrok tunnel (fallback for when the phone is off that network,
|
||||
// or the LAN URL below is stale).
|
||||
// Whichever responds first with a real backend (not a dead route/tunnel
|
||||
// error page) wins; if neither does, LAN is used as the default so error
|
||||
// messages point at the expected local backend rather than a dead tunnel.
|
||||
//
|
||||
|
||||
// LAN / WiFi / Emulator URL (port 8000 = nginx gateway)
|
||||
// - Physical Device on same Wi-Fi: use 'http://192.168.70.7:8000/api/v1'
|
||||
// - Physical Device on same Wi-Fi: use the backend machine's current LAN IP
|
||||
// - Android Emulator: use 'http://10.0.2.2:8000/api/v1'
|
||||
// - iOS Simulator: use 'http://localhost:8000/api/v1'
|
||||
static const String _lanBaseUrl = 'http://192.168.100.6:8000/api/v1';
|
||||
static const String _lanBaseUrl = 'http://192.168.80.84:8000/api/v1';
|
||||
|
||||
// Ngrok public tunnel URL (update ini setiap ngrok di-restart)
|
||||
static const String _ngrokBaseUrl = 'https://unlocated-waylon-potently.ngrok-free.dev/api/v1';
|
||||
@@ -22,52 +27,65 @@ class AppConfig {
|
||||
// Dynamic Base URL determined at startup
|
||||
static String apiBaseUrl = _lanBaseUrl;
|
||||
|
||||
/// Dynamic URL resolver: Checks if the Ngrok tunnel is active.
|
||||
/// If it returns a valid response (not 502/503/504), we use Ngrok.
|
||||
/// Otherwise, we fall back to the LAN/Local base URL.
|
||||
/// Dynamic URL resolver: tries the LAN backend first, then the public
|
||||
/// Ngrok tunnel, and picks whichever responds with a real backend.
|
||||
static Future<void> initializeApiBaseUrl() async {
|
||||
debugPrint('AppConfig: Checking local/LAN backend at $_lanBaseUrl...');
|
||||
if (await _isBackendReachable(_lanBaseUrl)) {
|
||||
apiBaseUrl = _lanBaseUrl;
|
||||
debugPrint('AppConfig: LAN backend is ACTIVE. API Base URL: $apiBaseUrl');
|
||||
return;
|
||||
}
|
||||
debugPrint('AppConfig: LAN backend not reachable, trying public Ngrok tunnel at $_ngrokBaseUrl...');
|
||||
|
||||
if (await _isBackendReachable(_ngrokBaseUrl)) {
|
||||
apiBaseUrl = _ngrokBaseUrl;
|
||||
debugPrint('AppConfig: Ngrok tunnel is ACTIVE. API Base URL: $apiBaseUrl');
|
||||
return;
|
||||
}
|
||||
|
||||
apiBaseUrl = _lanBaseUrl;
|
||||
debugPrint('AppConfig: Neither LAN nor Ngrok reachable. Defaulting to LAN API Base URL: $apiBaseUrl');
|
||||
}
|
||||
|
||||
/// Probes [baseUrl]/auth/login with a throwaway request to check whether a
|
||||
/// real backend answers behind it - not a timed-out connection, a dead
|
||||
/// tunnel, or a router/error page pretending to be a response.
|
||||
static Future<bool> _isBackendReachable(String baseUrl) async {
|
||||
final client = HttpClient();
|
||||
client.connectionTimeout = const Duration(seconds: 2);
|
||||
try {
|
||||
debugPrint('AppConfig: Checking public Ngrok tunnel at $_ngrokBaseUrl/auth/login...');
|
||||
final uri = Uri.parse('$_ngrokBaseUrl/auth/login');
|
||||
final uri = Uri.parse('$baseUrl/auth/login');
|
||||
final request = await client.postUrl(uri);
|
||||
|
||||
// Add headers to bypass the ngrok browser warning page
|
||||
|
||||
// Bypasses the Ngrok browser-warning interstitial; harmless for LAN.
|
||||
request.headers.add('ngrok-skip-browser-warning', 'true');
|
||||
request.headers.contentType = ContentType.json;
|
||||
request.write('{}');
|
||||
|
||||
|
||||
final response = await request.close();
|
||||
|
||||
|
||||
final contentType = response.headers.contentType?.mimeType ?? '';
|
||||
final ngrokErrorCode = response.headers.value('ngrok-error-code');
|
||||
|
||||
debugPrint('AppConfig: Ngrok response status: ${response.statusCode}, Content-Type: $contentType, Ngrok-Error-Code: $ngrokErrorCode');
|
||||
debugPrint('AppConfig: Probe $baseUrl -> status ${response.statusCode}, '
|
||||
'Content-Type: $contentType, Ngrok-Error-Code: $ngrokErrorCode');
|
||||
|
||||
// An active tunnel connected to our backend:
|
||||
// 1. Should not return an ngrok-error-code header.
|
||||
// A real backend behind this URL:
|
||||
// 1. Should not return an ngrok-error-code header (dead/offline tunnel).
|
||||
// 2. The response content-type should be JSON (our login API returns JSON).
|
||||
// 3. Status code should not be 502, 503, or 504.
|
||||
if (ngrokErrorCode == null &&
|
||||
contentType.contains('json') &&
|
||||
response.statusCode != 502 &&
|
||||
response.statusCode != 503 &&
|
||||
response.statusCode != 504) {
|
||||
apiBaseUrl = _ngrokBaseUrl;
|
||||
debugPrint('AppConfig: Ngrok tunnel is ACTIVE and backend responded. API Base URL: $apiBaseUrl');
|
||||
return;
|
||||
} else {
|
||||
debugPrint('AppConfig: Ngrok is offline, inactive, or returned an error page.');
|
||||
}
|
||||
// 3. Status code should not be 502, 503, or 504 (gateway/proxy errors).
|
||||
return ngrokErrorCode == null &&
|
||||
contentType.contains('json') &&
|
||||
response.statusCode != 502 &&
|
||||
response.statusCode != 503 &&
|
||||
response.statusCode != 504;
|
||||
} catch (e) {
|
||||
debugPrint('AppConfig: Ngrok connection failed or timed out: $e');
|
||||
debugPrint('AppConfig: Probe $baseUrl failed: $e');
|
||||
return false;
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
|
||||
apiBaseUrl = _lanBaseUrl;
|
||||
debugPrint('AppConfig: Using Local/LAN API Base URL: $apiBaseUrl');
|
||||
}
|
||||
|
||||
static const String loginEndpoint = '/auth/login';
|
||||
|
||||
Reference in new issue
Block a user