diff --git a/.planning/phases/07-design-system-polish/07-13-PLAN.md b/.planning/phases/07-design-system-polish/07-13-PLAN.md new file mode 100644 index 0000000..2bf727a --- /dev/null +++ b/.planning/phases/07-design-system-polish/07-13-PLAN.md @@ -0,0 +1,3239 @@ +--- +phase: 07-design-system-polish +plan: 13 +type: execute +wave: 1 +gap_closure: true +depends_on: + - 07-12 +files_modified: + - prowler-client/flutter/lib/src/l10n/strings.dart + - prowler-client/flutter/lib/src/widgets/horizon_sliver.dart + - prowler-client/flutter/lib/src/widgets/connect_dial.dart + - prowler-client/flutter/lib/src/widgets/error_banner.dart + - prowler-client/flutter/lib/src/state/connection_state.dart + - prowler-client/flutter/lib/src/state/error_state.dart + - prowler-client/flutter/lib/src/screens/home/home_screen.dart + - prowler-client/flutter/lib/src/screens/logs/logs_screen.dart + - prowler-client/flutter/lib/src/screens/servers/servers_screen.dart + - prowler-client/flutter/pigeons/prowler_platform.dart + - prowler-client/flutter/lib/src/generated/prowler_platform.g.dart + - prowler-client/flutter/android/app/src/main/kotlin/com/prowler/client/generated/ProwlerPlatform.g.kt + - prowler-client/flutter/android/app/src/main/kotlin/com/prowler/client/MainActivity.kt + - prowler-client/android/app/src/main/java/com/prowler/client/vpn/ProwlerVpnService.kt + - prowler-client/flutter/test/widget/connect_dial_test.dart + - prowler-client/flutter/test/widget/error_banner_test.dart + - prowler-client/flutter/test/widget/no_config_nudge_test.dart + - prowler-client/flutter/test/widget/a11y_semantics_test.dart + - .planning/debug/AP4-EXCEPTION-D-07-16-START-FAILED-DISCONNECTED-SUPPRESSION.md + - .planning/phases/07-design-system-polish/designer-delivery-5-redline-v3-ping/PING.md + - .planning/phases/07-design-system-polish/07-07-SUMMARY.md + - .planning/phases/07-design-system-polish/07-13-SUMMARY.md + - .planning/ROADMAP.md + - .planning/STATE.md +autonomous: false +requirements: + - REQ-14 + - REQ-15 + +user_setup: + - service: design-system-team + why: "B-1 horizon-sliver halo composition reset requires a spec amendment (redline v3) before implementation. Plan-author composes the ping; user sends + waits for designer response; tactical close (Wave 8a) can ship without B-1, full close (Wave 8b) blocks on amendment + implementation." + env_vars: [] + dashboard_config: + - task: "Review .planning/phases/07-design-system-polish/designer-delivery-5-redline-v3-ping/PING.md, ship to design-system team, await redline v3 amendment, then re-dispatch plan to implement against amended geometry" + location: "Designer comm channel" + +must_haves: + truths: + - "Home screen renders WITHOUT the `Home` title (PROWLER eyebrow + dial as headline only)" + - "Splash hold on AVD cold-launch ≤ 1000ms wall-time from launcher tap to Home first-frame (target 800ms; ceiling 1000ms accommodates AVD measurement noise)" + - "strings.dart no longer carries `Strings.statusIdle` consumers on the Home dial-meta (`statusIdle` either removed entirely or its Home-screen call site removed); LAYER chip never renders `— · idle` em-dash placeholder pair; EXIT chip never renders bare em-dash; connected meta row suppresses empty `exitCode`/`latencyMs` (no literal `exit · ms`); EXIT chip on L1 reads `direct` and drops the ` ms` suffix when latency is null; `Strings.serversSection` reads `One server per layer.`" + - "Connected meta row never renders the literal text `exit · ` (two spaces around the divider) or ends with bare ` ms` when latencyMs is null (B-2)" + - "On three consecutive `ProwlerNative.start()` failures (xray-core retry exhaustion), Dart receives ONE `error` Status (no immediate `idle`/`disconnected` clobber) AND a `ConnectError(code: connectFailed)` flows through `errorEventsProvider` AND `ErrorBanner` renders `Strings.errConnectFailedHeading` + `Strings.errConnectFailedBody` (B-3)" + - "Dart-side `errorLatchTransformer` in `connection_state.dart` (~20 LOC defense-in-depth — `INVESTIGATION-260516-blockers.md` Item 3 SCOPE REDUCER calls it the `~30 LOC of insurance`) latches the dial to `DialState.error` when observing `[connecting, error, idle]` without an intervening `connected` frame. The D-07-16 Kotlin lift means the trailing `idle` shouldn't fire — but if a future regression reintroduces the disconnected broadcast, the Dart latch keeps the dial in error and the test-case-B widget test trips. THIS is the regression gate for the AP-4 exception (B-3)." + - "Tapping the dial with a config where `validate(mode:)` returns `'L config missing for mode'` shows a SnackBar carrying `Strings.errNoConfigNudge` AND navigates to `/settings` via `context.go('/settings')` (B-4)" + - "Dial Semantics.label interpolates the dial's textual state (e.g. `Connect to VPN. Currently Tap to prowl.` / `Connect to VPN. Currently Connecting….`) — NOT the bare `_label(state)` value (A-1)" + - "Home info IconButton has tooltip + Semantics.label (A-2); Logs copy IconButton has Semantics.label (A-2)" + - "Per-field Reset hit-target on Servers screen ≥ 48dp logical (A-3 — Material 48dp minimum)" + - "AP-4 byte-freeze exception for `prowler-client/android/app/src/main/java/com/prowler/client/vpn/ProwlerVpnService.kt:256-303` (2-line edit per D-07-16) is documented at `.planning/debug/AP4-EXCEPTION-D-07-16-START-FAILED-DISCONNECTED-SUPPRESSION.md` AND cited in the relevant task's commit subject" + - "After ALL v0.7.0 items land + UX team unconditional sign-off on the v4.2 re-shoot bundle (full close), Phase 7 closes: ROADMAP 07-07 + 07-12 + 07-13 flip to [x]; STATE.md `total_plans: 40` (v2.0 in-scope: 4+16+7+13 — rev-2 WARNING W3 locked convention), `completed_plans` advances 36→40 (+1 W3 recount for the previously-excluded P6 plan + 3 closures for 07-07/07-12/07-13), `completed_phases` 6→7, `percent` 95→100" + artifacts: + - path: ".planning/debug/AP4-EXCEPTION-D-07-16-START-FAILED-DISCONNECTED-SUPPRESSION.md" + provides: "Formal AP-4 exception record for the D-07-16 2-line Kotlin lift (precedent: 5 prior AP4-EXCEPTION-*.md docs in same directory)" + contains: "D-07-16" + min_lines: 30 + - path: ".planning/phases/07-design-system-polish/designer-delivery-5-redline-v3-ping/PING.md" + provides: "Copy-paste-ready markdown body the user ships to design-system team requesting redline v3 amendment for B-1 horizon-sliver halo composition reset" + contains: "HORIZON-SLIVER-REDLINE" + min_lines: 60 + - path: "prowler-client/flutter/pigeons/prowler_platform.dart" + provides: "ConnectErrorCode.connectFailed enum value (D-07-17) — Kotlin emits it via ErrorEventListener when `ProwlerNative.start()` raises after exhausted xray retries; Dart maps it to errConnectFailed* strings" + contains: "connectFailed" + - path: "prowler-client/flutter/lib/src/state/connection_state.dart" + provides: "Dart-side `errorLatchTransformer` (~20 LOC defense-in-depth per rev-2 plan-checker BLOCKER B1 fix). Latches the dial to error when observing `[connecting, error, idle]` without an intervening `connected` frame; regression-gates the D-07-16 AP-4 Kotlin lift even if it reverts." + contains: "errorLatchTransformer" + min_lines: 80 + - path: "prowler-client/flutter/test/widget/no_config_nudge_test.dart" + provides: "Widget test for B-4 — empty-config dial-tap surfaces SnackBar(errNoConfigNudge) + navigates to /settings via go_router" + min_lines: 40 + - path: "prowler-client/flutter/test/widget/a11y_semantics_test.dart" + provides: "a11y semantics regression test — 4 testWidgets cases: A-1 dial Semantics.label state interpolation, A-2 Home info IconButton + Logs copy IconButton Semantics.label (rev-2 WARNING W2: locked tests via IconButton.tooltip → Tooltip → Semantics.label auto-forward), A-3 ProwlInput reset ≥48dp touch target" + min_lines: 80 + - path: ".planning/phases/07-design-system-polish/07-07-SUMMARY.md" + provides: "07-07 (Designer asset integration) close record — written at Wave 8b after v4.1 re-shoot UX team unconditional sign-off" + min_lines: 20 + - path: ".planning/phases/07-design-system-polish/07-13-SUMMARY.md" + provides: "This plan's close record (Wave 8b)" + min_lines: 40 + key_links: + - from: "prowler-client/flutter/lib/src/widgets/connect_dial.dart" + to: "prowler-client/flutter/lib/src/l10n/strings.dart" + via: "Strings.errNoConfigNudge SnackBar text + Strings.dialIdle/dialConnecting/dialConnected/dialError state interpolation in Semantics.label" + pattern: "errNoConfigNudge|Currently \\$\\{?_label" + - from: "prowler-client/flutter/lib/src/widgets/connect_dial.dart" + to: "prowler-client/flutter/lib/app.dart (go_router)" + via: "context.go('/settings') after empty-config validation issue surfaces" + pattern: "context\\.go\\(['\"]/settings['\"]\\)" + - from: "prowler-client/flutter/pigeons/prowler_platform.dart" + to: "prowler-client/flutter/lib/src/widgets/error_banner.dart" + via: "Pigeon-regen of ConnectErrorCode.connectFailed feeds error_banner _templateFor switch case mapping to Strings.errConnectFailedHeading/Body" + pattern: "case pigeon\\.ConnectErrorCode\\.connectFailed" + - from: "prowler-client/android/app/src/main/java/com/prowler/client/vpn/ProwlerVpnService.kt" + to: "prowler-client/flutter/lib/src/state/connection_state.dart" + via: "Skipping the disconnected broadcast on start-failed prevents the error→idle clobber on the Pigeon onStatusChanged stream" + pattern: "start-failed" + - from: "prowler-client/flutter/lib/src/state/connection_state.dart" + to: "prowler-client/flutter/test/widget/connect_dial_test.dart" + via: "Dart-side errorLatchTransformer is asserted by widget-test case B (`[connecting, error, idle]` → dial latches error). If the D-07-16 Kotlin lift reverts, the test still passes because the Dart-side latch holds; this IS the regression gate." + pattern: "errorLatchTransformer|test case B" + - from: "prowler-client/flutter/lib/src/widgets/error_banner.dart" + to: "prowler-client/flutter/lib/src/l10n/strings.dart" + via: "connectFailed case returns (Strings.errConnectFailedHeading, Strings.errConnectFailedBody, ...) with btnRetry + btnSwitchLayer actions" + pattern: "errConnectFailedHeading" +--- + + + + + +Close the v0.7.0 ship list — the UX team's CONDITIONAL sign-off at +`.planning/phases/07-design-system-polish/designer-delivery-4-ux-team-review/UX-REVIEW.md` +plus the B-4 gap surfaced by `INVESTIGATION-260516-blockers.md`. Eight items +total: three firm blockers (B-1 halo composition, B-2 empty meta-row, B-3 +silent-fail wiring), four polish must-fixes (§1 drop Home title, §2 splash +hold, §3 5 copy rewrites, §4a a11y trio), and one investigation-surfaced +partial (B-4 no-config nudge). + +The locked decisions in `07-CONTEXT.md ## v0.7.0 Gap Closure` are NON-NEGOTIABLE: + * **D-07-15** — B-1 fix path = redline v3 amendment FIRST, then implementation + (spec-amend-before-deviate keeps the spec→impl chain clean). Wave 7 ships + the ping; full Phase-7 close (Wave 8b) blocks on amendment + impl. + * **D-07-16** — B-3 fix path = AP-4 byte-freeze EXCEPTION on + `ProwlerVpnService.kt:256-303` (Wave 3, 2-line Kotlin edit collapses B-3 + from ~120 LOC Dart-side latch transformer to ~30 LOC). Precedent: 5 AP-4 + exceptions already granted in Phase 6 per `06-LEARNINGS.md` lines 124-134. + Cited in commit subject + new `.planning/debug/AP4-EXCEPTION-D-07-16-*.md` + record. INVESTIGATION caveat: the addresses cited in UX-REVIEW are + `:281-303`; per file inspection the `stopLocked()` private function spans + 256-303 — the actual edit lives in `stopLocked()` which is called from the + catch block at line 262 via `stopLocked("start-failed")`. The fix is to + suppress the trailing `statusSink.disconnected(reason)` call (line 301) + when `reason == "start-failed"` so the error frame at line 257 isn't + immediately clobbered. + **rev-2 BLOCKER B1 fix:** Wave 4 (Task 6) ALSO adds a ~20 LOC Dart-side + `errorLatchTransformer` in `lib/src/state/connection_state.dart` — + described in `INVESTIGATION-260516-blockers.md` Item 3 as the "~30 LOC of + insurance" SCOPE REDUCER caller. This is defense-in-depth: the Kotlin + lift alone suffices for the B-3 user-visible bug, but if a future + regression reintroduces the disconnected broadcast, the Dart-side latch + keeps the dial in error. The latch is REGRESSION-GATED by widget-test + case B: `[connecting, error, idle]` → dial latches `DialState.error` + (test passes even if Kotlin lift reverts, because Dart-side latch holds). + * **D-07-17** — B-3 ConnectError = add new `ConnectErrorCode.connectFailed` + enum value to `pigeons/prowler_platform.dart` (regenerates both .g.dart + and .g.kt; `pigeons/` is allowed under AP-4 because it's not Phase-5 + routing/dispatch source). Kotlin `ErrorEventListener` synthesises and + pushes the `ConnectError(code: connectFailed)` on `start-failed` so the + Dart `errorEventsProvider` surfaces it via the existing Pigeon stream. + * **D-07-18** — B-4 no-config signal shape = `ScaffoldMessenger.showSnackBar` + with `Strings.errNoConfigNudge` + `context.go('/settings')`. + +Wave structure (8 waves, with B-1 async-blocker split into 7→8a→8b): + * **Wave 1** (3 parallel trivial tasks): §1 drop Home title, §2 splash hold + cut, §3 5 copy rewrites + * **Wave 2** (B-2): empty-value suppression in Home dial-meta + quick-row + * **Wave 3** (B-3 platform + AP-4 doc): D-07-16 2-line Kotlin lift on + `ProwlerVpnService.kt` + D-07-17 Pigeon enum add + regen + AP-4-EXCEPTION + doc — sequenced INSIDE one task so commit-atomicity matches the lift + * **Wave 4** (B-3 Dart): error_banner._templateFor maps connectFailed → + errConnectFailed*; widget test asserts the connecting→error sequence + surfaces ErrorBanner (regression-gates the clobber) + * **Wave 5** (B-4): no-config SnackBar + auto-nav to /settings; widget test + * **Wave 6** (§4a a11y): A-1 dial Semantics state interpolation; A-2 + info/copy IconButton tooltip+Semantics; A-3 Servers reset-button 48dp + touch-target audit + bump + * **Wave 7** (B-1 async-blocker handoff): PING.md composition; user-handoff + checkpoint (autonomous:false) + * **Wave 8a** (tactical close): re-shoot v4.1 bundle covering only screens + affected by Waves 1-6 (02-home, 02e-home-dial-connected, 02d-home-dial-error + triggered via B-3, 04-/04b-/04c-servers); UX team confirms 7-of-8 blockers + resolved + * **Wave 8b** (full close, post-B-1): after design redline v3 amendment + lands AND B-1 implemented in a follow-up plan AND v4.2 re-shoot UX-team + unconditional sign-off — write 07-07-SUMMARY.md + 07-13-SUMMARY.md, flip + ROADMAP 07-07/07-12/07-13 to [x], STATE.md progress 36→40 (+1 W3 recount + 3 closures) / percent 95→100 at full close. Phase 7 closes. + +Total ~190 LOC across 11 source + 4 test files + 2 doc artifacts + 1 +AP-4-exception record + the 4 close-time meta-files. Tactical close (Wave 8a) +unblocks v0.7.0 ship of 7 items; full close (Wave 8b) caps Phase 7 after the +B-1 round-trip. + +Purpose: close UX team's conditional sign-off on v0.7.0; close Phase 7; unblock +Phase 8 (Windows) which UX team explicitly cleared to start as soon as B-1/B-2/B-3 +land per UX-REVIEW.md §7. + +Output: 8 items resolved, ~190 LOC merged, Phase 7 closed via Wave 8b, ROADMAP ++ STATE updated, AP-4 exception formally recorded. + + + +@$HOME/.claude/get-shit-done/workflows/execute-plan.md +@$HOME/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/07-design-system-polish/07-CONTEXT.md +@.planning/phases/07-design-system-polish/designer-delivery-4-ux-team-review/UX-REVIEW.md +@.planning/phases/07-design-system-polish/INVESTIGATION-260516-blockers.md +@.planning/phases/06-flutter-ui-settings/06-LEARNINGS.md +@.planning/debug/AP4-EXCEPTION-BUG-B-FIX.md +@prowler-client/flutter/lib/src/l10n/strings.dart +@prowler-client/flutter/lib/src/state/connection_state.dart +@prowler-client/flutter/lib/src/state/connection_actions.dart +@prowler-client/flutter/lib/src/state/error_state.dart +@prowler-client/flutter/lib/src/widgets/connect_dial.dart +@prowler-client/flutter/lib/src/widgets/error_banner.dart +@prowler-client/flutter/lib/src/widgets/horizon_sliver.dart +@prowler-client/flutter/lib/src/screens/home/home_screen.dart +@prowler-client/flutter/lib/src/screens/logs/logs_screen.dart +@prowler-client/flutter/lib/src/screens/servers/servers_screen.dart +@prowler-client/flutter/pigeons/prowler_platform.dart +@prowler-client/android/app/src/main/java/com/prowler/client/vpn/ProwlerVpnService.kt +@prowler-client/flutter/android/app/src/main/kotlin/com/prowler/client/MainActivity.kt +@prowler-client/flutter/lib/app.dart +@prowler-client/flutter/lib/src/services/prowler_config_validation.dart +@prowler-client/flutter/test/widget/connect_dial_test.dart +@prowler-client/flutter/test/widget/error_banner_test.dart + + + + +From `prowler-client/flutter/lib/src/l10n/strings.dart` (current strings touched +by §3 + B-4): +```dart +// Existing — touched by §3: +static const String statusIdle = 'idle'; // §3 row 1: remove from Home dial-meta consumer +static const String quickBypassExitDash = '—'; // §3 rows 2-3: stop using on idle Home quick-row +static const String serversSection = 'Endpoints. One per layer.'; // §3 row 5: rewrite to 'One server per layer.' + +// Existing — B-3 surfaces these via the new ConnectErrorCode.connectFailed mapping: +static const String errConnectFailedHeading = 'Connection failed.'; +static const String errConnectFailedBody = + 'We tried 3 times. Switch layer or check your network.'; + +// NEW string to add (B-4 per D-07-18): +// static const String errNoConfigNudge = 'Paste a vless:// config to start.'; +``` + +From `prowler-client/flutter/lib/src/state/connection_actions.dart` lines 30-48 +(the call site whose return is currently discarded in connect_dial.dart): +```dart +Future> connect(pigeon.Mode mode) async { + final model.ProwlerConfig cfg = + await ref.read(configRepositoryProvider.future); + final List issues = cfg.validate(mode: mode); + if (issues.isNotEmpty) { + return issues; // ← returned but discarded at connect_dial.dart:186-191 + } + final pigeon.ProwlerConfig pigeonCfg = _toPigeonConfig(cfg); + try { + await pigeon.ProwlerHostApi().connect(mode, pigeonCfg); + return const []; + } on PlatformException catch (e) { + return ['${e.code}: ${e.message ?? "(no message)"}']; + } +} +``` + +From `prowler-client/flutter/lib/src/services/prowler_config_validation.dart` +lines 26-49 (validation reasons — used to discriminate "config missing" vs +"config malformed" per INVESTIGATION-260516-blockers.md Item 1): +```dart +// Returns 'L1 config missing for Direct mode' / 'L2 config missing for Bypass mode' +// / 'L3 config missing for Relay mode' when the target mode has no Lx slot — +// this is the "no-config" branch B-4 must catch. +// Other returned strings ('server address is empty', '... must be bare host', +// 'port out of range', '... UUID is not v4 shape') are malformed-config cases +// that already get surfaced by the existing connect-then-Pigeon-error path. +``` + +From `prowler-client/flutter/lib/src/widgets/connect_dial.dart` lines 185-191 +(the B-4 fix site — currently throws away the validation issues): +```dart +} else { + // Idle/error → connect using the user's most recent layer + // choice (defaults to Mode.direct on first launch). + final pigeon.Mode target = ref.read(lastSelectedModeProvider); + await ref + .read(connectionActionsProvider.notifier) + .connect(target); + // ← B-4 fix lands here: capture the returned List, branch on + // `.contains('config missing for')`, fire SnackBar + go('/settings'). +} +``` + +From `prowler-client/flutter/lib/src/widgets/connect_dial.dart` lines 172-175 +(the A-1 Semantics fix site — current label is `_label(state)`, i.e. just the +state string, not the full interpolated sentence): +```dart +return Semantics( + label: _label(state), // ← A-1 fix: change to "Connect to VPN. Currently ${_label(state)}." + button: true, + child: GestureDetector( +``` + +From `prowler-client/android/app/src/main/java/com/prowler/client/vpn/ProwlerVpnService.kt` +lines 256-303 (the D-07-16 AP-4-exception edit site). The relevant flow: + * Line 256-264: catch (t: Throwable) emits `statusSink.error(...)` THEN + calls `stopLocked("start-failed")` (line 262) and rethrows. + * Line 281-303: `stopLocked(reason)` — final lines (300-302): + ```kotlin + currentMode = MODE_NONE + if (hadSession) { + dependencies.statusSink.disconnected(reason) + } + ``` + * D-07-16 LIFT (2-line edit): change line 301 from + `if (hadSession) {` to `if (hadSession && reason != "start-failed") {` + OR equivalently add a `reason != "start-failed"` clause. The condition + skips the `disconnected("start-failed")` broadcast that immediately + clobbers the error frame. + * After this lift, Dart's `connection_state.dart` re-emits ONLY the `error` + Status (no trailing idle), so the dial latches `DialState.error` + naturally. + +From `prowler-client/flutter/android/app/src/main/kotlin/com/prowler/client/ErrorEventListener.kt` +(the Kotlin synthesis path for B-3 — read line 1 to confirm shape; if it +exposes a `pushError(ConnectError)` or equivalent, use it from +ProwlerHostApiImpl's connect-catch to inject the new connectFailed code. If +the listener is pure-passive over an EventChannel sink, add a sibling +synthesiser following the same Pigeon `@EventChannelApi onError()` shape. +Executor inspects + reports the actual shape; the task body has fallback +guidance. + +From `prowler-client/flutter/lib/src/widgets/error_banner.dart` lines 91-125 +(the exhaustive `_templateFor` switch that needs the new connectFailed case +AFTER pigeon regen lands the enum value — see Wave 3/4 sequencing): +```dart +(String, String, List) _templateFor( + BuildContext context, WidgetRef ref, pigeon.ConnectErrorCode code, +) { + switch (code) { + case pigeon.ConnectErrorCode.networkUnavailable: … + case pigeon.ConnectErrorCode.serverUnreachable: … + case pigeon.ConnectErrorCode.coreCrashed: … + case pigeon.ConnectErrorCode.consentDenied: … + case pigeon.ConnectErrorCode.switchFailed: … + // ← Wave 4 adds: + // case pigeon.ConnectErrorCode.connectFailed: + // return (Strings.errConnectFailedHeading, + // Strings.errConnectFailedBody, + // [ _btn(Strings.btnRetry, () => _retry(ref)), + // _btn(Strings.btnSwitchLayer, () => context.go('/layers')) ]); + } +} +``` + +From `prowler-client/flutter/pigeons/prowler_platform.dart` lines 29-35 +(the enum that Wave 3 extends — Pigeon 26.3.2 canonical Dart enum syntax): +```dart +enum ConnectErrorCode { + networkUnavailable, + serverUnreachable, + coreCrashed, + consentDenied, + switchFailed, + // ← Wave 3 adds: `connectFailed,` after switchFailed +} +``` +Regen command (per 07-12 precedent + Phase 6 06-02 SUMMARY): +```bash +cd prowler-client/flutter && \ + dart run pigeon --input pigeons/prowler_platform.dart +``` +This rewrites: + - `lib/src/generated/prowler_platform.g.dart` + - `android/app/src/main/kotlin/com/prowler/client/generated/ProwlerPlatform.g.kt` +DO NOT hand-edit those files — they appear in `files_modified:` only because +the regen step rewrites them. + +From `prowler-client/flutter/lib/src/screens/home/home_screen.dart` lines +86-103 (the §1 fix site — the `Home` title literal lives on line 88 in the +`TopBar(...)` invocation): +```dart +TopBar( + eyebrow: 'PROWLER', + title: 'Home', // ← §1: change to '' (empty) or remove the param if TopBar accepts nullable + right: IconButton( + icon: const Icon(LucideIcons.info, size: 20, color: ProwlerTokens.fog), + onPressed: () => showAboutProwlerSheet(context), // ← A-2: add Semantics.label / tooltip + ), +), +``` +Investigate `TopBar`'s `title` param signature (likely `String title` not +`String? title`); if non-nullable, pass empty string `''` and the TopBar +should render no title row (verify the TopBar widget collapses on empty +string — if it renders an empty band, also tweak TopBar to suppress the slot +when empty; treat as a small TopBar refactor in the same task). + +From `prowler-client/flutter/lib/src/screens/home/home_screen.dart` lines +129-194 (B-2 fix sites — the _buildDialMeta + _buildQuickRow methods): + + * Line 134-160 (connected branch): currently always renders + `'exit '` + code + ` · ` + `'$ms ms'` — when code is empty AND ms is + empty, the user sees literally `exit · ` and ` ms`. B-2 fix: branch + inside the connected case on `code.isNotEmpty && status.exitCode != null` + + `status.latencyMs != null` and suppress the empty parts. + * Line 196-213 (_buildQuickRow): currently renders layerValue + `'— · idle'` when `activeMode == null` (idle state) and exitValue from + `quickBypassExitDash` (the em-dash). §3 + B-2 fix: when state == idle, + show `'not connected'` instead of `'— · idle'`; when state == idle, the + EXIT chip shows nothing (hide) or `'not connected'`; when state == + connected with `activeMode == Mode.direct` AND `exitCountry == null`, + show `'direct'` as the EXIT value (NOT `'— · ms'`). + +From `prowler-client/flutter/lib/src/screens/logs/logs_screen.dart` lines +97-109 (A-2 fix site — copy IconButton): +```dart +right: asyncBuf.maybeWhen( + data: (List lines) => IconButton( + icon: const Icon(LucideIcons.copy, size: 20, color: ProwlerTokens.fog), + onPressed: lines.isEmpty ? null : () => _copyToClipboard(lines), + tooltip: 'Copy logs to clipboard', // ← tooltip already present, but + // need an explicit + // Semantics(label:) wrap OR + // IconButton's tooltip already + // feeds Semantics by default — + // confirm with TalkBack/Semantics + // debugger first + ), + orElse: () => const SizedBox.shrink(), +), +``` + +From `prowler-client/flutter/lib/src/screens/servers/servers_screen.dart` +(the A-3 fix site — per-field Reset icons live inside `ProwlInput`'s +`onResetPressed` slot. The reset icon button widget lives in +`prowler-client/flutter/lib/src/widgets/prowl_input.dart`; executor reads +that file in Wave 6 Task 3, confirms the existing icon button's +hit-area dimensions, and either: + (a) wraps the icon in an `IconButton` with `iconSize: 20, padding: + EdgeInsets.all(14)` so total touch-target = 20 + 2*14 = 48dp, OR + (b) wraps in `SizedBox(width: 48, height: 48, child: …)` if it's a bare + InkResponse — pick whichever matches the existing widget shape. +NOTE: UX team's A-3 is scoped to Servers; the same Reset icon may appear on +other screens (Settings) — fix at the ProwlInput widget level so all consumers +inherit the touch-target compliance. + +From `prowler-client/flutter/android/app/src/main/kotlin/com/prowler/client/MainActivity.kt` +(the §2 fix site — splash hold). Current MainActivity does NOT call +`installSplashScreen()` (the Android 12+ SplashScreen API entry point); it +extends `FlutterFragmentActivity` and the Android-12 splash hold is +controlled by `windowSplashScreenAnimationDuration` (defaults to 1000ms, +configured in `android/app/src/main/res/values-v31/styles.xml` if set; if +absent, Android picks the duration based on cold-start latency, typically +~1.6s on AVD per UX-REVIEW frame_02 measurement). + +§2 fix options (investigate in Wave 1): + (a) **Declarative (preferred):** add + `800` + to `LaunchTheme` in BOTH `values-v31/styles.xml` AND + `values-night-v31/styles.xml`. Cleanest fix; no MainActivity edit. + Note Android caps animation-duration at 1000ms (anything higher is + clamped); 800ms is in-range. + (b) **Programmatic:** call `installSplashScreen().setKeepOnScreenCondition { false }` + in MainActivity.onCreate BEFORE super.onCreate (per + androidx.core.splashscreen 1.0+ contract); requires adding the + `androidx.core:core-splashscreen` Gradle dep. +Pick (a) — pure resource edit, no Gradle/Kotlin churn, and the +declarative duration is exactly the UX team's specified value. + +NOTE on splash hold measurement: the UX team's "~1.6s" is the wall-clock +between launcher-tap and Home first-frame. The Android-12 splash window +itself has TWO durations: + (i) `windowSplashScreenAnimationDuration` (icon fade-in, declarative, 0-1000ms) + (ii) the post-engine-init hand-off (Flutter's first-frame draw triggers + splash hide via `FlutterNativeSplash.preserve()`/`.remove()` if used, + OR the system removes splash on Activity-ready). + +Project does NOT use `FlutterNativeSplash.preserve()`/`.remove()` in main.dart +(grepped — only the codegen `dart run flutter_native_splash:create` step +appears, which configures the Android-12 splash declaratively). So splash +auto-hides on first-frame; cutting (i) from 1000→800ms reclaims ~200ms + +relies on the engine cold-start to do the rest. Validate empirically in +Wave 8a re-shoot. + +From `prowler-client/flutter/test/widget/connect_dial_test.dart` lines 21-30 +(test-double pattern for ConnectionStatus — Wave 4 B-3 widget test reuses +this pattern): +```dart +class _FakeConnStatus extends ConnectionStatus { + _FakeConnStatus(this._fakeStatus); + final pigeon.Status _fakeStatus; + @override + Stream build() => Stream.value(_fakeStatus); +} +``` +For B-3 test (multi-frame stream): change `Stream.value(...)` to +`Stream.fromIterable([connecting, error])` and add a `pumpAndSettle()` to +ensure both frames flush. + +From `prowler-client/flutter/test/widget/error_banner_test.dart` lines 31-40 +(test-double pattern for ErrorEvents — reuse for Wave 4 B-3 widget test): +```dart +class _FakeErrEvents extends ErrorEvents { + _FakeErrEvents(this._fake); + final pigeon.ConnectError _fake; + @override + Stream build() => + Stream.value(_fake); +} +``` + +From `.planning/debug/AP4-EXCEPTION-BUG-B-FIX.md` (template for the new +D-07-16 exception doc — read first to mirror format/structure exactly). + + + + + + + + + Task 1 (Wave 1a — §1 drop Home title): Remove `Home` title from HomeScreen TopBar — keep PROWLER eyebrow + info icon; verify TopBar collapses cleanly when title is empty + + prowler-client/flutter/lib/src/screens/home/home_screen.dart, + prowler-client/flutter/lib/src/widgets/top_bar.dart + + +**Step 1 — Inspect TopBar's `title` param contract.** Read +`prowler-client/flutter/lib/src/widgets/top_bar.dart` and confirm: + (a) Whether `title` is `String` (non-nullable, requires `''` or sentinel) + or `String?` (nullable, requires `null` and conditional rendering). + (b) Whether TopBar currently renders an empty band / placeholder when title + is empty string, or already collapses cleanly. + +**Step 2 — Edit HomeScreen TopBar invocation** at +`prowler-client/flutter/lib/src/screens/home/home_screen.dart` line 86-89. +Current: +```dart +TopBar( + eyebrow: 'PROWLER', + title: 'Home', + right: IconButton(...), +), +``` + +If TopBar's `title` is `String?`: change to `title: null`. +If TopBar's `title` is `String`: change to `title: ''` AND modify TopBar +widget to suppress the title slot when the string is empty (use a small +`if (title.isNotEmpty)` guard around the title Text widget — pattern +matches the existing `if (right != null)` slot pattern if present). + +This implements UX-REVIEW §1 — "Kill the `Home` title. Keep `Connect` as the +tab label." The tab label `Strings.navConnect` is in `bottom_nav.dart`'s +NavigationBar and is NOT touched by this task (it's already correct). + +Per the alternative spec from UX-REVIEW §1 final paragraph: "If you want to +keep a title for nav-stack reasons, make it `Tap to prowl` and remove the +duplicate caption under the dial." DO NOT take this alternative path — the +plan-author confirms with UX-REVIEW that the dial caption is the load-bearing +copy (it's the dial's affordance), so dropping Home wins over duplicating +Tap-to-prowl. The dial caption stays. + +Per CLAUDE.md "extension over duplication": modify the existing TopBar +widget's title slot once rather than creating a TopBarNoTitle variant. Other +screens that pass `title: 'Layers'` etc. continue rendering their title +normally — only the empty/null case is added. + +**No new strings** — `Strings.navConnect` already exists (line 75) and is +unchanged; the Home screen TopBar simply drops its title. + +**REQ-14 lexicon check:** no new user-visible strings, no forbidden vocab +introduced. + +**Per D-07-14 light-touch:** trivial Flutter-lib-only edit; no AP-4 +considerations. + + + + bash -c ' + set -euo pipefail + cd /home/parf/projects/work/prowler/prowler-client/flutter + # Gate 1: Home screen TopBar no longer passes title: "Home". + ! grep -nE "title:\s*[\x27\x22]Home[\x27\x22]" lib/src/screens/home/home_screen.dart + # Gate 2: HomeScreen build() still contains PROWLER eyebrow + info icon. + grep -q "eyebrow:\s*[\x27\x22]PROWLER[\x27\x22]" lib/src/screens/home/home_screen.dart + grep -q "LucideIcons.info" lib/src/screens/home/home_screen.dart + # Gate 3: dart analyze clean on the touched files. + flutter analyze lib/src/screens/home/home_screen.dart lib/src/widgets/top_bar.dart + # Gate 4: widget tests still pass (no regression on existing Home tests). + flutter test test/widget/connect_dial_test.dart --reporter compact + echo "Task 1 (§1) gates PASS" + ' + + + + HomeScreen TopBar invocation no longer carries `title: "Home"`; the + eyebrow `PROWLER` and the info IconButton remain. TopBar widget either + collapses cleanly on null/empty title or is patched to do so. `flutter + analyze` clean. UX-REVIEW §1 closed. + + + + + Task 2 (Wave 1b — §2 splash hold): Cut Android 12+ splash hold from ~1.6s to ~800ms via declarative `windowSplashScreenAnimationDuration` in values-v31/styles.xml + values-night-v31/styles.xml + + prowler-client/flutter/android/app/src/main/res/values-v31/styles.xml, + prowler-client/flutter/android/app/src/main/res/values-night-v31/styles.xml + + +**Step 1 — Add `windowSplashScreenAnimationDuration` to BOTH v31 styles.xml.** + +`values-v31/styles.xml` currently carries: +```xml + +``` + +Use the Edit tool to add `800` +right after the `windowSplashScreenAnimatedIcon` line (matching the indent). + +Apply the IDENTICAL edit to `values-night-v31/styles.xml`. + +NOTE: Android caps `windowSplashScreenAnimationDuration` at 1000ms. 800ms is +in-range per the Android-12 SplashScreen API docs +(https://developer.android.com/develop/ui/views/launch/splash-screen#duration). +The animated icon plays for this duration; once it completes, Android hands +off to the Activity. Combined with Flutter cold-start (~600-800ms on AVD per +07-12 measurement), the user-perceived splash window should land in the +800-1200ms range (UX team target was 800ms — ~200ms reduction from current +~1000ms+ engine cold-start, total window cut by ~600ms; ceiling 1000ms in the +must_haves accommodates AVD measurement noise). + +Per the in-file flutter_native_splash.yaml comment, this XML file is one of +the FOUR auto-generated by `flutter_native_splash:create` — BUT +`flutter_native_splash` 2.4.7 does NOT manage `windowSplashScreenAnimationDuration`, +so a manual addition is preserved across regens. If a future regen DOES wipe +it (worth confirming once via diff), guard with a sed re-injection step in +the regen wrapper — but at v0.7.0 ship, the hand-add is sufficient. + +**Step 2 — DO NOT add `androidx.core:core-splashscreen` Gradle dep** (the +alternative programmatic `installSplashScreen().setKeepOnScreenCondition` +path). Declarative wins because: (a) no Gradle churn, (b) no MainActivity +edit, (c) matches the existing flutter_native_splash declarative model. + +**Step 3 — DO NOT touch `main.dart` or `FlutterNativeSplash.preserve()`** — +the project does not use `preserve()`/`remove()` (grepped at plan time — +zero hits in `lib/main.dart`). The Flutter side already auto-hides on +first-frame; cutting the Android-side animation duration is the right lever. + +**Verification deferred to Wave 8a re-shoot** — the wall-clock measurement +from launcher-tap to Home first-frame requires AVD cold-launch (covered in +Wave 8a). The automated verify here is a structural grep that the duration +landed; the empirical splash-hold ≤ 1000ms is a Wave 8a re-shoot +acceptance criterion. + +**Per D-07-14 light-touch:** asset-config only, no AP-4 considerations. + + + + bash -c ' + set -euo pipefail + cd /home/parf/projects/work/prowler/prowler-client/flutter + # Gate 1: BOTH v31 styles.xml files carry the duration attribute set to 800. + grep -q "android:windowSplashScreenAnimationDuration\">800<" \ + android/app/src/main/res/values-v31/styles.xml + grep -q "android:windowSplashScreenAnimationDuration\">800<" \ + android/app/src/main/res/values-night-v31/styles.xml + # Gate 2: the value lands inside LaunchTheme, not NormalTheme. + # Confirm via awk that the line appears between