docs(07): import design UX team review (designer-delivery-4) — conditional sign-off

Design UX team responded to v4 UX bundle 2026-05-16:  CONDITIONAL sign-off
for v0.7.0 ship + Phase 7 close. Phase 8 (Windows) can start as soon as the
3 v0.7.0 blockers land — other v0.7.0 items don't gate Phase 8 kickoff.

=== TL;DR per Ask ===

Ask 1 (tab/title split):   bless + KILL "Home" title (1 piece of copy that
                           doesn't sound like Prowler; dial is its own headline)
Ask 2 (first-launch):      bless + cut splash hold 1.6s→800ms; 1 open
                           question on no-config path (could become blocker)
Ask 3 (voice/copy):        bless 90% + 5 line rewrites (drop "idle" meta,
                           em-dash placeholders, "Endpoints"→"servers")
Ask 4a (a11y):            ⏸ defer full pass + 3 must-fix (dial content-desc,
                           icon labels, Reset touch targets)
Ask 4b (Servers density): ⚠ reshape in v0.7.x (collapsed default + 1 Reset/group
                           + optional-field marking + paste affordance)

=== 3 firm v0.7.0 ship blockers ===

B-1 · Horizon-sliver halo on Home eats 30% of viewport (visual scale bug)
B-2 · Connected meta row reads literally "exit · ms" with no values (suppress
      empty fields)
B-3 · Bogus-IP / endpoint failure silently returns dial to idle. Surface
      dialError + errConnectFailed ErrorBanner. (Same as my bug-shaped #1 —
      UX team confirms it as a real UX gap, not intentional.) Wiring already
      exists in strings.dart + ErrorBanner; missing the connect-loop-drain
      → state transition.

=== Bug-shaped observations dispositions ===

#1 silent-fail → BLOCKS v0.7.0 (= B-3 above)
#2 8s drop-detect → v0.7.x (annoying not deceptive; investigate xray-core
   keepalive interval + add UI tunnel-health ping)

=== UNTRIGGERED states dispositions ===

02c connecting       → accept absence
02d dial-error       → does NOT exist; it's bug #1 in disguise — fix bug =
                       state captured
08-err-network       → accept code-review
08-err-connect-failed→ same as 02d / bug #1
08-err-server-unreach→ accept code-review
08-err-switch        → accept code-review

No real-device follow-up bundle requested.

=== v0.7.0 work list ===
- B-1, B-2, B-3 (3 firm blockers)
- §1 drop "Home" title
- §2 splash 1.6s→800ms
- §3 5 copy rewrites
- §4a A-1/A-2/A-3 a11y must-fixes
- §2 ¶4 verify no-config first-launch path (might add 1 blocker)

=== v0.7.x followup batch ===
- Full a11y pass (Accessibility Scanner + TalkBack)
- Servers screen reshape (4 changes)
- Bug #2 tunnel-drop detection
- Russian translation brief

Phase 7 close path: this is the "rewrites" branch from .continue-here.md
decision matrix. Plan needed to batch the v0.7.0 work before Phase 7 marks
complete. /gsd-plan-phase 7 --gaps OR multi-task /gsd-quick chain.

Source review document: designer-delivery-4-ux-team-review/UX-REVIEW.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 19:03:49 +02:00
parent ec330a92b1
commit 9649c7235b

View File

@@ -0,0 +1,209 @@
# UX team sign-off · Prowler v0.6.6 (phase-07 bundle v4)
**From:** Design UX team
**To:** Phase-7 owner / Flutter team
**Date:** 2026-05-16
**Build under review:** `prowler-debug-v0.6.6-multiabi.apk` (sha256 `af3667bc…`)
**Bundle:** `prowler-phase07-ux-team-v4` — 20 stills + 9 cold-launch frames + `UNTRIGGERED.md`
**Verdict:****Conditional sign-off.** Ships at v0.7.0 after the 3 blockers below land. Voice + first-launch flow blessed. Servers density flagged as a v0.7.x reshape, not a v0.7.0 blocker.
---
## TL;DR
| | Verdict |
|---|---|
| **Ask 1** · Connect tab / Home title split | ✅ Bless. Rename `Home` → drop it entirely. See §1. |
| **Ask 2** · First-launch zero-onboarding | ✅ Bless with one tweak. The dial IS the onboarding. See §2. |
| **Ask 3** · Voice / copy bets | ✅ Bless 90%. Five specific rewrites in §3. |
| **Ask 4a** · A11y | ⏸ Defer full pass to v0.7.x, with 3 must-fix items at v0.7.0. See §4a. |
| **Ask 4b** · Servers density | ⚠️ Reshape in v0.7.x. Not a v0.7.0 blocker — it's a power-user screen. See §4b. |
| **Bug-shaped #1** · Bogus-IP silent-fail | 🚫 **Blocks v0.7.0.** Real UX gap. See §5. |
| **Bug-shaped #2** · 8s tunnel-drop detection | ⏸ v0.7.x — annoying not blocking. See §5. |
| **Untriggered states** | Accept code-review evidence for 5/6. The 6th (02d dial-error) is bug #1 in disguise — fix the bug, capture the state. |
**Three blockers for v0.7.0 ship:**
- B-1 · Horizon-sliver halo on Home eats 30% of viewport. Scale it down per design-system spec. (Visual bug, not UX gap, but ships in this build.)
- B-2 · Connected-state Home meta row reads literally `exit · ms` with no values. Suppress empty fields.
- B-3 · Bogus-IP / endpoint failure silently returns dial to idle. Surface `dialError` ("Tap to retry") + an ErrorBanner.
Everything else is fine ship.
---
## §1 · Ask 1 — Tab label vs screen title
You asked: bless or kill the `Connect` / `Home` split.
**Kill the `Home` title. Keep `Connect` as the tab label.**
Two reasons:
1. The dial is its own headline. A 288dp glowing cat-in-a-ring doesn't need the word "Home" hovering over it — that's belt-and-suspenders for a one-screen surface.
2. "Home" is the single piece of copy in this whole app that doesn't sound like Prowler. It reads like a generic React Native starter. The rest of the voice is lowercase-confident; "Home" is title-case civilian.
**Specific change:** delete the `Home` line. Keep the `PROWLER` eyebrow. The screen becomes:
```
PROWLER (i)
[ horizon-sliver ]
[ DIAL ]
idle ← or Tap to prowl, see §3
[ LAYER ] [ EXIT ]
```
If you want to keep a title for nav-stack reasons, make it `Tap to prowl` and remove the duplicate caption under the dial. Don't say the same thing twice.
The tab label stays `Connect`. It's the right verb for the action and the right anchor for nav-bar muscle memory — every other VPN client labels its primary tab this way, and that's a feature not a bug.
---
## §2 · Ask 2 — First-launch zero-onboarding
You asked: does "Tap to prowl" + a giant unlabeled circle teach the flow?
**Yes, with one caveat that's not actually about onboarding.**
The cold-launch frames (`frames/01-cold-launch/frame_{00..08}.png`) tell a clean story: splash → Home → dial visible. The dial is *visually unambiguous* — it's the biggest thing on the screen, it pulses, and it says "Tap to prowl" with text. A first-time user does the right thing on muscle memory alone.
**What I'd change:** the splash hold (~1.6s before Home renders, per frame_02 timing). That's slow for an app whose value prop is "tap → tunnel." Cut to 800ms — long enough for the chromatic-aberration wordmark to register, short enough not to feel like a stalled launch.
**What I'd NOT change:** there's no need for a wizard, a "what's a layer?" intro card, or a permissions pre-screen. The dial → OS consent dialog → handshake is the right teach-by-doing. The bundle's `08-err-consent.png` shows the recovery path when the user taps Cancel; that's a sufficient safety net.
**One real risk** that's masked by the bundle: what happens on first launch when there's *no config loaded yet?* `00-launcher-home.png``02-home-screen.png` assumes a baked-in config. If a tester installs the APK and there's no `vless://` configured, tapping the dial should NOT just fail silently → it should bounce to Settings → Paste from clipboard with a one-line nudge. Confirm this is implemented, or this becomes a blocker.
---
## §3 · Ask 3 — Voice & copy
You asked: bless the set or mark specific lines for rewrite.
**Bless 90% of it.** The voice is the strongest thing about this build. `Pure vibe, zero function.` is a love letter to the user. `=^..^= thanks for being a friend.` is hand-on-heart correct. The L3 line — "Moscow → Amsterdam. Slower but stubborn." — does in 5 words what most VPN apps do in a paragraph and a diagram.
**5 specific lines to rewrite:**
| Where | Current | Proposed | Why |
|---|---|---|---|
| Dial meta, idle | `idle` | *(remove — the caption "Tap to prowl" already covers it)* | "idle" is dev-shorthand bleeding through. The user doesn't need a redundant state label under a button that already tells them what to do. |
| LAYER chip when idle | `— · idle` | `not connected` or hide chip values entirely until connected | Same problem — "idle" twice on one screen, plus the em-dash placeholder reads like missing data. |
| EXIT chip when idle | `—` | `not connected` or hide | Worse than the layer chip because the dash sits alone. |
| EXIT chip when on L1 (no remote exit) | `—` + label `exit · ms` | `direct` (in the value) and drop the `ms` unit when there's no value | Currently `02e-home-dial-connected.png` reads literally `exit · ms` with empty numerics underneath. Embarrassing in screenshots. **This is blocker B-2.** |
| Servers subtitle | `Endpoints. One per layer.` | `One server per layer.` | "Endpoints" is the only piece of network jargon that escapes containment. The tab is called Servers; the subtitle should match the tab's vocabulary, not the engineer's. |
**Don't rewrite:**
- `Tap to prowl` — perfect.
- `Connecting…` — perfect.
- `Tunnel up` — perfect (and the past-tense brevity matches the cat-walking visual).
- `Tap to retry` — perfect.
- `handshake ▓▒░` — top-tier brand voice, ship it.
- `Tunnel dropped` — perfect.
- `Local TCP tricks. No server needed.` — "tricks" lands; it's playful without being cute. Keep it.
- `Pick a layer.` + `Tap a layer to switch. Switching tears down and reconnects.` — this is two stacked imperatives but they're doing different jobs (title + helptext) and both are short. Fine.
- All the error-banner copy. The "Tap Allow on the next prompt." flow is exemplary.
**Russian translation note:** when `.arb` work starts, the only line that needs translator hand-holding is `=^..^= thanks for being a friend.` — that's a cultural reference (Golden Girls theme) that won't survive a literal translation. Brief the translator to write a Russian sign-off in the same register: warm, slightly retro, slightly inside-joke. Don't try to translate ASCII-cat emoticons either; let them stand.
---
## §4 · Ask 4 — A11y & Servers density
### 4a · A11y
**Defer the full WCAG 2.1 AA pass to v0.7.x.** Three items must-fix at v0.7.0:
| | Item | Why it's must-fix |
|---|---|---|
| A-1 | `content-description` on the dial. Currently a screen reader sees an unlabeled `GestureDetector`. Add: `"Connect to VPN. Currently {state}."` where `{state}` interpolates the dial's text label. | A 288dp control with no semantic label is unusable, not just inconvenient. |
| A-2 | `content-description` on the info icon top-right of Home + the copy icon top-right of Logs. Both currently auto-derive nothing because they're SVG-mask buttons. | Same root as A-1, smaller blast radius. |
| A-3 | Touch-target audit on the Reset arrows in Servers (`04b-servers-expanded.png`). They look ≤32dp on capture — below Android Material 48dp minimum. | Density problem AND a11y problem. Solve them together — see §4b. |
**Acceptance criteria for the v0.7.x full pass:** Android Accessibility Scanner clean + manual TalkBack run-through of the cold-launch → connect → switch-layer → disconnect happy path. Skip WCAG 2.1 AA formal audit unless a public release is planned (private distribution model from brief means we're not subject to it). Reduced-motion is already passing per `09`/`10`/`11`.
### 4b · Servers density
**Reshape in v0.7.x. Not a v0.7.0 blocker** — this is a power-user / tester surface, not a daily-use one.
Concerns from `04b-servers-expanded.png` (DIRECT group expanded, 8 advanced fields visible):
- 11+ field cards in a single scrollable column
- Each field has its own Reset arrow (top-right) — visually noisy, touch-target-undersized
- "Fingerprint" is empty and there's no signal that it's optional vs required
- The same pattern repeats for BYPASS and RELAY below the fold
**For v0.7.0:** ship as-is. Most users will paste a `vless://` URI from Settings and never see this screen.
**For v0.7.x reshape:** four changes, in priority order:
1. **First-run state of this screen = all groups collapsed.** A power user can expand. A new tester sees three named pills, taps the one they're configuring, expands. Defaults to friction-light.
2. **One Reset button per group**, bottom-right of each expanded panel. Drop the per-field Resets. (Fixes A-3 and the visual noise at once.)
3. **Mark optional fields explicitly.** `Fingerprint (optional)` in the label, or move them under a second `Show optional fields` toggle below `Show advanced`.
4. **Add a contextual "Paste vless://" affordance** at the top of this screen, not just in Settings. The user-paths-to-config-input shouldn't all funnel through Settings → scroll → paste card.
Density verdict for one-handed mobile: **the collapsed view (`04-servers-screen.png`) is fine.** The expanded view is what hurts, and the fix is to make expansion an explicit power-user choice with a single Reset button per group.
---
## §5 · Bug-shaped observations
You flagged these tentatively. Both are real.
### Bug #1 · Bogus-IP / endpoint failure silently returns dial to idle 🚫 BLOCKS v0.7.0
This is the same root cause as the untriggered `02d-home-dial-error` state. When the user pastes a bad config or their endpoint goes dark, the dial cycles through retries and then *silently* returns to `Tap to prowl`. The user has no signal anything went wrong.
**Required fix:**
- After N failed connect attempts (3? configurable?), surface the `dialError` state — red-tinted glow ring, "Tap to retry" caption, AND
- Below the dial, surface an ErrorBanner: `Connection failed.` / `We tried 3 times. Switch layer or check your network.` — exactly the `errConnectFailed` strings already in `strings.dart`.
The strings exist. The error banner component exists (we saw it in `08-err-consent.png`). The dial-error visual state exists in the design system. What's missing is the *wiring* from "connect attempt loop drained" → "show these things." That's not a design ask; it's a Flutter task. But this *blocks ship* because shipping a VPN client where failure modes are silent is bad UX, full stop.
### Bug #2 · Tunnel-drop detection takes >8s ⏸ v0.7.x
`Tunnel up` persists for 8+ seconds after airplane mode cuts underlying network on AVD. May be xray-core keepalive tuning, not Prowler logic. Annoying — a user looks at the dial, sees "Tunnel up," and their browser is timing out. But not deceptive in the way bug #1 is.
**v0.7.x:** investigate xray-core's keepalive interval + add a UI-side tunnel-health ping every 3-5s. If two pings fail, drop to `dialError`. Don't block v0.7.0 on this; document as known issue.
---
## §6 · Untriggered states (UNTRIGGERED.md disposition)
You asked: (a) bless strings as sufficient, (b) demand real-device captures, or (c) accept code-review.
**(a) + (c).** Specifically:
- 02c connecting (<100ms on AVD) → **accept absence.** The strings.dart inventory + the existing `Connecting…` design in the system are sufficient. If you want belt-and-suspenders, run one real-device capture on a 4G connection where handshake spans 200-500ms; that's not a blocker.
- 02d dial-error → **does NOT exist as an untriggered state.** It's bug #1. Fix the bug and the state becomes triggerable.
- 08-err-network → **accept code-review.** Strings + ErrorBanner component exist. Future real-device capture nice-to-have.
- 08-err-connect-failed → **same as 02d** — part of bug #1.
- 08-err-server-unreachable → **accept code-review.** Same as err-network.
- 08-err-switch → **accept code-review.** Lowest-priority error state (only triggers during mid-handshake layer-switch, narrow window).
**No real-device follow-up bundle requested.** Bug #1 is the only thing here that needs to ship-block v0.7.0; everything else is text-evidence-sufficient.
---
## §7 · What ships at v0.7.0 vs v0.7.x
**v0.7.0 (Phase 7 close):**
- B-1 · Scale horizon-sliver halo on Home to design-system spec (currently ~3× too dominant)
- B-2 · Suppress empty values + units in Home meta row (`exit · ms` bug)
- B-3 · Wire `dialError` + `errConnectFailed` to connect-loop drain (bug #1)
- §1 · Drop the `Home` screen title
- §2 · Cut splash hold from ~1.6s to ~800ms
- §3 · 5 copy rewrites (idle, em-dash placeholders, "endpoints" → "servers")
- §4a · A-1, A-2, A-3 (a11y must-fixes)
**v0.7.x (followup polish batch):**
- §4a · Full A11y pass against Android Accessibility Scanner + manual TalkBack
- §4b · Servers screen reshape (all 4 changes)
- Bug #2 · Tunnel-drop detection ping
- Voice nice-to-have: Russian translator brief for `.arb` work
Sign-off conditional on the v0.7.0 list above. Phase 8 (Windows) can start as soon as B-1, B-2, B-3 land — the other v0.7.0 items don't gate it.
---
## Closing
This is a build I'd be happy to put on my own phone after the v0.7.0 fixes land. The voice carries the brand single-handedly; the dial-as-onboarding is the right call; the layer model reads correctly on the first try. The cracks are all on the edges — error-state plumbing, one over-aggressive halo, one over-dense form. Nothing structural.
Good work. Forward to engineering.
— UX team