diff --git a/ssologin-android-context.png b/ssologin-android-context.png deleted file mode 100644 index 7fef3d0b..00000000 Binary files a/ssologin-android-context.png and /dev/null differ diff --git a/ssologin-desktop-context.png b/ssologin-desktop-context.png deleted file mode 100644 index 97667774..00000000 Binary files a/ssologin-desktop-context.png and /dev/null differ diff --git a/ssologin-mobile.png b/ssologin-mobile.png deleted file mode 100644 index 8c36732b..00000000 Binary files a/ssologin-mobile.png and /dev/null differ diff --git a/userfront/lib/core/i18n/toml_asset_loader.dart b/userfront/lib/core/i18n/toml_asset_loader.dart index 4c0c9ff1..9ab97076 100644 --- a/userfront/lib/core/i18n/toml_asset_loader.dart +++ b/userfront/lib/core/i18n/toml_asset_loader.dart @@ -37,6 +37,7 @@ Map _normalizeFlatTranslations(Map flatMap) => bool _isUserfrontTranslationKey(String key) { return key.startsWith('domain.') || + key.startsWith('err.userfront.') || key.startsWith('msg.userfront.') || key.startsWith('ui.userfront.') || key.startsWith('ui.common.'); diff --git a/userfront/lib/core/services/log_policy.dart b/userfront/lib/core/services/log_policy.dart index df8e3c24..0aa1b834 100644 --- a/userfront/lib/core/services/log_policy.dart +++ b/userfront/lib/core/services/log_policy.dart @@ -52,14 +52,11 @@ class LogPolicy { required String? appEnv, required String? productionDebugFlag, }) { - final flag = parseOptionalBoolFlag(productionDebugFlag); - if (flag.specified) { - return flag.enabled; - } if (!isProductionEnv(appEnv)) { return true; } - return false; + final flag = parseOptionalBoolFlag(productionDebugFlag); + return flag.specified && flag.enabled; } static bool shouldRelayClientLog({ @@ -67,10 +64,12 @@ class LogPolicy { required String? appEnv, required String? productionDebugFlag, }) { - if (debugEnabled( - appEnv: appEnv, - productionDebugFlag: productionDebugFlag, - )) { + final flag = parseOptionalBoolFlag(productionDebugFlag); + final debugRelayEnabled = isProductionEnv(appEnv) + ? flag.specified && flag.enabled + : !(flag.specified && !flag.enabled); + + if (debugRelayEnabled) { return true; } final normalized = level.trim().toUpperCase(); diff --git a/userfront/scripts/dev-server.sh b/userfront/scripts/dev-server.sh index 2e1691e1..a17fe2d4 100644 --- a/userfront/scripts/dev-server.sh +++ b/userfront/scripts/dev-server.sh @@ -5,16 +5,98 @@ cd /workspace /bin/sh ./scripts/sync_userfront_locales.sh cd /workspace/userfront +USERFRONT_INTERNAL_PORT="${USERFRONT_INTERNAL_PORT:-5000}" +USERFRONT_FLUTTER_RUN_FLAGS="${USERFRONT_FLUTTER_RUN_FLAGS:---debug}" +USERFRONT_BOOT_WARMUP_ATTEMPTS="${USERFRONT_BOOT_WARMUP_ATTEMPTS:-120}" +USERFRONT_BOOT_WARMUP_INTERVAL_SECONDS="${USERFRONT_BOOT_WARMUP_INTERVAL_SECONDS:-0.5}" +USERFRONT_BOOT_WARMUP_LOCALES="${USERFRONT_BOOT_WARMUP_LOCALES:-ko en}" +USERFRONT_BOOT_WARMUP_VIEWPORTS="${USERFRONT_BOOT_WARMUP_VIEWPORTS:-mobile:390 desktop:1440}" + +warm_get() { + path="$1" + locale="$2" + viewport="$3" + width="${viewport#*:}" + if [ "$width" = "$viewport" ]; then + width="" + fi + + wget -qO- \ + --header="Accept-Language: $locale" \ + --header="Viewport-Width: $width" \ + "http://127.0.0.1:${USERFRONT_INTERNAL_PORT}${path}" >/dev/null 2>&1 +} + +warm_userfront_once() { + flutter_pid="$1" + attempt=1 + started_at="$(date +%s)" + + while [ "$attempt" -le "$USERFRONT_BOOT_WARMUP_ATTEMPTS" ]; do + if wget -qO- "http://127.0.0.1:${USERFRONT_INTERNAL_PORT}/flutter_bootstrap.js" >/dev/null 2>&1; then + break + fi + if ! kill -0 "$flutter_pid" 2>/dev/null; then + echo "[userfront-boot] warmup skipped because flutter exited before readiness" >&2 + return 0 + fi + attempt=$((attempt + 1)) + sleep "$USERFRONT_BOOT_WARMUP_INTERVAL_SECONDS" + done + + if [ "$attempt" -gt "$USERFRONT_BOOT_WARMUP_ATTEMPTS" ]; then + echo "[userfront-boot] warmup skipped after ${USERFRONT_BOOT_WARMUP_ATTEMPTS} readiness attempts" >&2 + return 0 + fi + + echo "[userfront-boot] one-shot warmup starting locales=\"${USERFRONT_BOOT_WARMUP_LOCALES}\" viewports=\"${USERFRONT_BOOT_WARMUP_VIEWPORTS}\"" >&2 + + for locale in $USERFRONT_BOOT_WARMUP_LOCALES; do + for viewport in $USERFRONT_BOOT_WARMUP_VIEWPORTS; do + warm_get "/${locale}/signin" "$locale" "$viewport" || true + done + done + + for asset in \ + / \ + /flutter_bootstrap.js \ + /main.dart.mjs \ + /main.dart.wasm \ + /canvaskit/skwasm.js \ + /canvaskit/skwasm.wasm \ + /canvaskit/skwasm_heavy.js \ + /canvaskit/skwasm_heavy.wasm \ + /assets/AssetManifest.bin.json \ + /assets/FontManifest.json + do + wget -qO- "http://127.0.0.1:${USERFRONT_INTERNAL_PORT}${asset}" >/dev/null 2>&1 || true + done + + finished_at="$(date +%s)" + elapsed_seconds=$((finished_at - started_at)) + echo "[userfront-boot] one-shot warmup completed in ${elapsed_seconds}s" >&2 +} + set -- flutter run \ -d web-server \ --web-hostname 0.0.0.0 \ - --web-port "${USERFRONT_INTERNAL_PORT:-5000}" \ + --web-port "${USERFRONT_INTERNAL_PORT}" \ --wasm \ --dart-define=BACKEND_URL="${BACKEND_URL:-}" \ --dart-define=CLIENT_LOG_DEBUG="${CLIENT_LOG_DEBUG:-false}" \ --dart-define=APP_ENV="${APP_ENV:-dev}" \ --dart-define=USERFRONT_URL="${USERFRONT_URL:-}" \ - ${USERFRONT_FLUTTER_RUN_FLAGS:-} \ + ${USERFRONT_FLUTTER_RUN_FLAGS} \ --no-web-resources-cdn -exec "$@" +"$@" & +flutter_pid="$!" + +terminate() { + kill "$flutter_pid" 2>/dev/null || true + wait "$flutter_pid" 2>/dev/null || true +} + +trap terminate INT TERM +warm_userfront_once "$flutter_pid" +wait "$flutter_pid" diff --git a/userfront/scripts/optimize-web-build.mjs b/userfront/scripts/optimize-web-build.mjs index 5de34a56..75a03f95 100644 --- a/userfront/scripts/optimize-web-build.mjs +++ b/userfront/scripts/optimize-web-build.mjs @@ -78,15 +78,18 @@ const canvasKitConfig = 'config:{canvasKitBaseUrl:"canvaskit/"}'; bootstrap = bootstrap.replace( /_flutter\.loader\.load\(\{\s*serviceWorkerSettings:\s*(\{[^{}]*\})\s*,\s*config:\s*\{[\s\S]*?serviceWorkerUrl[\s\S]*?\}\s*,\s*config:\s*\{[^}]*\}\s*\}\);/g, - `_flutter.loader.load({${canvasKitConfig}});`, + (_match, settings) => + `_flutter.loader.load({serviceWorkerSettings:${ensureServiceWorkerUrl(settings)},${canvasKitConfig}});`, ); bootstrap = bootstrap.replace( /_flutter\.loader\.load\(\{\s*serviceWorkerSettings:\s*(\{[^{}]*\})\s*,\s*config:\s*\{[^}]*\}\s*\}\);/g, - `_flutter.loader.load({${canvasKitConfig}});`, + (_match, settings) => + `_flutter.loader.load({serviceWorkerSettings:${ensureServiceWorkerUrl(settings)},${canvasKitConfig}});`, ); bootstrap = bootstrap.replace( /_flutter\.loader\.load\(\{\s*serviceWorkerSettings:\s*(\{[^{}]*\})\s*\}\);/g, - `_flutter.loader.load({${canvasKitConfig}});`, + (_match, settings) => + `_flutter.loader.load({serviceWorkerSettings:${ensureServiceWorkerUrl(settings)},${canvasKitConfig}});`, ); bootstrap = bootstrap.replace( /_flutter\.loader\.load\(\);/g, @@ -302,4 +305,34 @@ async function cacheFirst(request) { `; } +function ensureServiceWorkerUrl(settings) { + const serviceWorkerUrl = `"/flutter_service_worker.js?v=" + ${serviceWorkerVersionExpression(settings)}`; + if (/serviceWorkerUrl\s*:/.test(settings)) { + return settings.replace( + /serviceWorkerUrl\s*:\s*[^,\n}]+,?/, + `serviceWorkerUrl: ${serviceWorkerUrl},`, + ); + } + + const closingBraceIndex = settings.lastIndexOf('}'); + if (closingBraceIndex < 0) { + return settings; + } + const beforeClosing = settings.slice(0, closingBraceIndex).trimEnd(); + const afterClosing = settings.slice(closingBraceIndex); + const separator = + beforeClosing.endsWith('{') || beforeClosing.endsWith(',') ? '' : ','; + return `${beforeClosing}${separator} + serviceWorkerUrl: ${serviceWorkerUrl}, + ${afterClosing}`; +} + +function serviceWorkerVersionExpression(settings) { + const match = settings.match(/serviceWorkerVersion\s*:\s*([^,\n}]+)/); + return ( + match?.[1]?.replace(/\/\*[\s\S]*?\*\//g, '').trim() ?? + 'serviceWorkerVersion' + ); +} + console.log(`[userfront] optimized ${basename(buildDir)} with hashed entrypoints and brotli assets`); diff --git a/userfront/test/log_policy_test.dart b/userfront/test/log_policy_test.dart index f53e69a8..fb8a49ec 100644 --- a/userfront/test/log_policy_test.dart +++ b/userfront/test/log_policy_test.dart @@ -14,7 +14,7 @@ void main() { ); }); - test('explicit debug flag applies in development-like environment', () { + test('explicit true enables debug in development-like environment', () { expect( LogPolicy.debugEnabled(appEnv: 'dev', productionDebugFlag: 'true'), isTrue, @@ -23,13 +23,16 @@ void main() { LogPolicy.debugEnabled(appEnv: 'development', productionDebugFlag: '1'), isTrue, ); + }); + + test('explicit false does not suppress local debug in development', () { expect( LogPolicy.debugEnabled(appEnv: 'dev', productionDebugFlag: 'false'), - isFalse, + isTrue, ); expect( LogPolicy.debugEnabled(appEnv: 'development', productionDebugFlag: '0'), - isFalse, + isTrue, ); }); diff --git a/userfront/test/toml_asset_loader_test.dart b/userfront/test/toml_asset_loader_test.dart index 313fe4e6..f66294bf 100644 --- a/userfront/test/toml_asset_loader_test.dart +++ b/userfront/test/toml_asset_loader_test.dart @@ -43,6 +43,10 @@ void main() { expect(translations['ui.admin.nav.api_keys'], isNull); expect(translations['ui.dev.console_title'], isNull); + expect( + translations['err.userfront.auth_proxy.login_failed'], + 'Login failed.', + ); expect(translations['ui.userfront.login.action.submit'], 'Sign in'); expect(translations['ui.common.theme_light'], 'Light'); }, diff --git a/userfront/web/index.html b/userfront/web/index.html index 2950294f..a8f5692d 100644 --- a/userfront/web/index.html +++ b/userfront/web/index.html @@ -122,7 +122,7 @@ - +