426 lines
27 KiB
Markdown
426 lines
27 KiB
Markdown
# Mobile UX audit results — 2026-10-07
|
||
|
||
The extended direct-chat audit makes new-device message reliability the
|
||
first priority: a cached OMEMO device list can omit a newly added browser,
|
||
leaving new messages unreadable while the sender sees a read tick. Encrypted
|
||
direct attachments are also offered but cannot be sent. Group information,
|
||
drafts and reading-position preservation remain frequent-task gaps. Core
|
||
text delivery works with a current device list; the successful initial file
|
||
checks were group uploads, not encrypted direct attachments.
|
||
|
||
This is an agent-led exploratory audit and documentation-based comparison,
|
||
not a user study or a completed real-iPhone PWA certification. No application
|
||
fix or production deployment was performed.
|
||
|
||
## Environment and evidence
|
||
|
||
- User's main device: iPhone 17 Pro, iOS 26.6, installed PWA.
|
||
- Automated engines: Chromium 152.0.7977.82 and Linux Playwright WebKit 26.5.
|
||
The latter is not installed Safari/iOS 26.6. Touch-enabled contexts used
|
||
a 402 × 874 viewport; WebKit used Playwright's iPhone 13 user-agent.
|
||
- Additional viewport checks: 320 × 700, 874 × 402 and 1280 × 800. They
|
||
exercise responsive layout, not actual desktop/mobile OS integration.
|
||
- Target: `https://chat.0ff.space`, bundle `assets/index-DLDjjWaX.js`, SHA-256
|
||
`f153a8723ae24c14db115f725c24bd7cab3a0355695d4430efef2ffc19978f23`.
|
||
Both engines fetched that same bundle. Its bytes match the active nginx
|
||
root `/nix/store/lqny4y6afnm1mvq234995x53xcd8lwnw-0ff-chat-0.1.0`.
|
||
The package source's `src/` and `public/` match the local checkout at
|
||
`4eb726340a6448fba81fc91ec10bbeee65863d6b`. The deployed Git revision
|
||
itself was not independently established; the bundle hash identifies it.
|
||
- Dedicated accounts A/B and test room are named in [UX-AUDIT.md](UX-AUDIT.md).
|
||
All sent messages and uploads were synthetic, within those accounts.
|
||
- Chromium full report: `/tmp/chat-ux-runner/results/chromium-1791380418245/report.json`.
|
||
- Corrected Chromium reconnect report:
|
||
`/tmp/chat-ux-runner/results/chromium-1791380751412/report.json`.
|
||
- WebKit full report: `/tmp/chat-ux-runner/results/webkit-1791380800982/report.json`.
|
||
- Extended Chromium direct/PWA report:
|
||
`/tmp/chat-ux-runner/results/direct-pwa-chromium-1791385881194/report.json`.
|
||
- Extended WebKit direct/PWA report:
|
||
`/tmp/chat-ux-runner/results/direct-pwa-webkit-1791386884704/report.json`.
|
||
- Extended runs use the same bundle/hash above and private persistent A
|
||
profiles. Standalone media/navigator indicators are injected; this is not
|
||
an actual Home Screen installation. Real browser storage, workers and
|
||
the live app are exercised. Chromium-only freeze/push/voice tests are
|
||
labeled separately below. Profile data includes authentication/key
|
||
material; keep the entire runtime directory private and outside Git.
|
||
- Each full-report directory contains screenshots of group headers,
|
||
message actions, four group layouts, settings, attachments and the
|
||
fingerprint dialog. Runtime evidence is outside Git and subject to /tmp
|
||
cleanup; the findings below preserve the observations in the repository.
|
||
- Protocol evidence: `/tmp/chat-ux-runner/room-info.json`, queries made as
|
||
both owner and joined ordinary member of the dedicated test room.
|
||
|
||
## Prioritized findings
|
||
|
||
### P1 — new direct-chat devices are missed; read ticks can mislead (D00)
|
||
|
||
Reproduce in both engines: open A's conversation with B, caching B's
|
||
existing devices. Then log B into a new browser profile and send a new
|
||
message from A. A's cached list omits B's current device ID. B displays
|
||
"Could not decrypt this message." for the new live message, while A can
|
||
show `read`. This is distinct from the expected undecryptable backlog on
|
||
a device that did not exist when old messages were sent.
|
||
|
||
Diagnostic evidence: each report compares the cached public device IDs
|
||
with B's current ID. Deleting only the B device-list entry in A's private
|
||
test profile and reloading allows new messages to decrypt. Subsequent
|
||
successful delivery/read/link checks explicitly depend on that test
|
||
intervention; it is not a production fix or normal user workflow.
|
||
`cachedDeviceList` returns an IndexedDB entry indefinitely, without a
|
||
freshness check; `markDisplayed` can mark an undecryptable incoming item
|
||
as displayed. These source observations support the reproduced behavior.
|
||
|
||
Proposal: refresh device lists when peers add/remove devices, with a
|
||
bounded freshness policy/fallback and clear key/device status. Decide
|
||
what a read marker means when the content cannot be decrypted; avoid
|
||
implying successful reading. Never fall back to plaintext to hide failures.
|
||
|
||
Acceptance: add a device after an existing conversation is open, send in
|
||
both directions without clearing storage, and verify the new device can
|
||
read messages; remove a device and verify encryption targets update.
|
||
Undecryptable content must have accurate status and a useful recovery path.
|
||
|
||
### P1 — encrypted direct media is offered but rejected (D06/D08)
|
||
|
||
Both engines: select a synthetic PNG and text file in the direct composer.
|
||
Each fails with "Encrypted file attachments aren't supported yet."
|
||
Chromium's synthetic microphone also supports recording and discard, but
|
||
sending a recording fails with the same message. Voice recording was not
|
||
repeated in WebKit or with real iPhone permissions/hardware.
|
||
|
||
This is a known missing capability with misleading availability. Source
|
||
`sendFile` uploads the blob before `send` rejects its attachment URL for an
|
||
OMEMO-capable peer. Do not describe the successful group-file audit as
|
||
evidence that encrypted direct media works.
|
||
|
||
Proposal: communicate unsupported media before file selection/recording
|
||
and before uploading. Full encrypted attachment support needs a compatible
|
||
protocol/client design; never remove the rejection by sending plaintext.
|
||
Acceptance: supported flows send/play back; unsupported flows explain
|
||
the limitation without recording/uploading data that cannot be sent.
|
||
|
||
### P1 — offline cold start hides local history and needs retry (P07)
|
||
|
||
In the extended Chromium run, close the browser, reopen its persistent
|
||
profile with networking offline, and navigate to the direct chat. The
|
||
cached shell returns HTTP 200, but the app displays the login form instead
|
||
of locally stored messages. After connectivity is restored, the form
|
||
remains for the ten-second observation window. Reload restores the saved
|
||
session and chat without entering a password.
|
||
|
||
Source `App.svelte` mounts Main only with a session JID and non-offline
|
||
status, and attempts saved-token resume once on mount. The successful
|
||
in-session reconnect check is a different case from cold startup.
|
||
|
||
Proposal: show locally stored conversations in a clearly offline state,
|
||
keep sending unavailable, and retry the stored session on restored
|
||
connectivity or through an explicit Retry action. Acceptance: offline
|
||
startup exposes saved history, explains connectivity, retains drafts and
|
||
recovers without requiring an unexplained browser reload.
|
||
|
||
### P1 — group information and people actions are missing (G01/G03)
|
||
|
||
Reproduce: open a group and tap its name/count. Nothing opens. Tap the
|
||
nickname above an incoming group message: no person/contact action opens.
|
||
Both engines reproduce these results. `Chat.svelte` renders the group title
|
||
as a static div and sender nickname as a span; the group menu only contains
|
||
Invite and Leave.
|
||
|
||
Impact: a user cannot answer "Who is in this group?" or find a person from
|
||
the conversation through familiar navigation. This is a feature gap.
|
||
|
||
Proposed behavior: make the name/avatar/count an accessible button opening
|
||
group info. Show group name, description, people and available actions.
|
||
Selecting a person opens supported actions such as a room-private chat;
|
||
show contact actions only when the real identity is available and permitted.
|
||
Keep nickname, account identity and membership roles distinct.
|
||
|
||
Acceptance: one tap reaches group info; participants are individually
|
||
selectable; back restores the chat, draft and reading position; owner,
|
||
admin and ordinary-member views are tested independently.
|
||
|
||
### P1 — unsubmitted text is lost on navigation (N02)
|
||
|
||
Reproduce: type a draft, use Back to chats, reopen the same room. The
|
||
composer is empty in both engines. The component owns `draft` in memory
|
||
and is remounted during navigation.
|
||
|
||
Impact: switching conversations can discard text without notice.
|
||
The extended direct-chat checks reproduce the loss through both the chat
|
||
list and settings. An unsent direct draft is also lost after a persistent
|
||
browser restart, even though automatic login and stored messages return.
|
||
|
||
Proposed behavior: store a draft per account/conversation and restore it
|
||
when opening that conversation. Clear it only after successful sending or
|
||
explicit deletion. Show a draft indicator in the chat list. Cross-device
|
||
draft synchronization is a separate extension, not required for this fix.
|
||
|
||
Acceptance: switching chat, settings and browser back preserves text and
|
||
multiline content; chats never share drafts; failed sends retain text.
|
||
Decide and document reload persistence and logout deletion explicitly.
|
||
|
||
### P1 — reopening a chat jumps away from the reading position (N01)
|
||
|
||
Reproduce: scroll up in a room with synthetic history, return to the list,
|
||
reopen it. Both engines jump to the bottom. Chromium's measured remaining
|
||
scroll distance changes from 2,014 px to 0; WebKit's from 4,441 px to 0.
|
||
The component's first load always calls `scrollToBottom`.
|
||
|
||
Proposed behavior: retain a per-conversation message anchor and offset;
|
||
preserve it across navigation. Offer an explicit jump-to-latest control
|
||
when reading older messages and new messages arrive.
|
||
|
||
Acceptance: return to the same visible message after switching views;
|
||
incoming messages, older-history loading and image resizing do not
|
||
unexpectedly move the reader. First-time opening/unread positioning needs
|
||
a deliberate policy rather than applying restoration to every situation.
|
||
|
||
### P1 candidate — no quoted reply action (M02)
|
||
|
||
Browser observation: tapping and invoking a context-menu gesture on an
|
||
incoming message exposes no application reply action. Source inspection
|
||
finds no quoted-reply/swipe-reply implementation or message action menu.
|
||
Native iOS long press and native text selection were not exercised.
|
||
|
||
Impact: group replies cannot visibly identify which earlier message they
|
||
refer to. This is a feature gap, not a failed delivery mechanism.
|
||
|
||
Proposal: a discoverable Reply action and optional swipe gesture, quoted
|
||
composer preview with cancel, and a tap on the sent quote to reach the
|
||
original message. First establish compatible XMPP representation/fallback
|
||
before implementing cross-client behavior.
|
||
|
||
### P2 — menu and modal keyboard handling (A02)
|
||
|
||
Both engines: Escape does not close More actions; opening the fingerprint
|
||
dialog leaves focus outside it; Tab can reach controls behind the dialog;
|
||
Escape does not close it either.
|
||
|
||
Proposal: move focus into the modal, keep it there while open, close with
|
||
Escape and return focus to the trigger. Implement the appropriate menu
|
||
focus/navigation behavior. This also matters to assistive input users.
|
||
|
||
### P2 candidate — touch areas below the preferred phone size (A01)
|
||
|
||
Measured in the group view: More actions 34 × 34 CSS px; back, attachment
|
||
and record 36 × 36. They exceed the 24 px WCAG 2.2 AA size threshold by
|
||
size alone; these measurements do not justify claiming an AA violation.
|
||
The stronger target-size criterion uses 44 × 44 CSS px. The proposal is
|
||
to aim for that larger target for frequent phone actions without enlarging
|
||
every icon. [W3C minimum](https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html),
|
||
[W3C enhanced](https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html).
|
||
|
||
Real-phone comfort and spacing still require validation. No horizontal
|
||
page overflow was measured in the tested group/settings layouts.
|
||
|
||
### P2 candidates — discovery, search and per-chat controls
|
||
|
||
Source/UI inventory shows New chat as a raw account/room-address input,
|
||
without a contact picker or guided group creation; no conversation/history
|
||
search; no per-chat mute option in the examined menus/settings. Assess
|
||
frequency with the operator before implementing these extensions.
|
||
|
||
Suggested order: contact picker and a clear Join/Create group distinction,
|
||
then per-chat mute and local history search. Avoid a full messenger-feature
|
||
catalog before fixing navigation and group information.
|
||
|
||
## Membership constraint: a frontend list needs correct semantics
|
||
|
||
The current `participants` number comes from a set of occupants observed
|
||
through MUC presence, not a full membership query. In this audit the count
|
||
stayed at 2 even after one browser left; earlier hibernated sessions can
|
||
retain the same account's nickname. It is unsuitable as proof that all
|
||
members are online or that there are only two members.
|
||
|
||
Live XMPP queries after joining the test room showed:
|
||
|
||
| Requester | Member list | Admin list | Owner list |
|
||
| --- | --- | --- | --- |
|
||
| Owner A | B returned as member | Empty | A returned as owner |
|
||
| Ordinary member B | forbidden | forbidden | forbidden |
|
||
|
||
Therefore implement available occupant information first, and query all
|
||
membership affiliations only when allowed. A member-only query would omit
|
||
owners/admins. If the operator wants every ordinary member to see absent
|
||
members too, review the room's anonymity/list permissions as an explicit
|
||
product/privacy decision; do not silently expose identities or pretend a
|
||
permission error means an empty group. XEP-0045 makes member-list access
|
||
dependent on room configuration. [XEP-0045 member lists](https://xmpp.org/extensions/xep-0045.html#modifymember).
|
||
|
||
## Comparator research
|
||
|
||
Checked official sources on 2026-10-07. No comparator app was logged into;
|
||
installed app versions and hands-on gesture behavior are not verified.
|
||
These are documented patterns supporting proposals, not visual parity
|
||
claims about current releases.
|
||
|
||
| Task | Officially documented pattern | Implication for 0ff chat |
|
||
| --- | --- | --- |
|
||
| Inspect group | Signal mobile opens group settings from the group name; members and admin labels are available there. [Signal](https://support.signal.org/hc/en-us/articles/360050427692-Manage-a-group) | Make header an entry to group info and people, with roles. |
|
||
| Preserve unfinished text | Telegram documents peer-specific drafts and cross-device synchronization. [Telegram](https://core.telegram.org/api/drafts) | At least preserve local per-chat drafts and show their existence. |
|
||
| Reply to a message | Telegram documents swipe-to-reply and navigating from a quote to its original. [Telegram](https://telegram.org/tour/groups) | Add quoted replies and a return path to the original message. |
|
||
| Reply or contact a group sender | Signal documents swipe-to-reply and avatar → Message for a private conversation. [Signal](https://support.signal.org/hc/en-us/articles/6851465208986-Reply-to-a-specific-message) | Give message/person actions discoverable entry points. |
|
||
| Manage interruptions on iOS | Signal exposes timed per-chat muting through the chat header. [Signal](https://support.signal.org/hc/en-us/articles/360043273491-In-App-Notification-Options) | Consider per-chat mute alongside global notification controls. |
|
||
| WhatsApp group/reply workflows | Official articles exist for adding/removing group members and replying to messages; search-indexed group instructions mention opening group info via the subject. [Members](https://faq.whatsapp.com/841426356990637/), [Reply](https://faq.whatsapp.com/3240283752856917/) | Useful secondary references; full article bodies did not render in the research fetch, so exact iPhone steps remain unverified. |
|
||
|
||
## Successful checks and limits
|
||
|
||
- Both accounts logged in; group messages arrived; synthetic text-file
|
||
uploads appeared for the recipient; Leave room followed by rejoining
|
||
worked in both engines. Invitations were already verified in the initial
|
||
trial, not independently repeated for new membership in this round.
|
||
- Direct messages arrived with the OMEMO lock visible in both engines.
|
||
This is a UI flow check, not a new cryptographic or Monal interop audit.
|
||
A newly created WebKit profile showed the expected undecryptable backlog
|
||
placeholder for messages sent before that device existed.
|
||
- The corrected interruption test injects a WebSocket close and blocks
|
||
reconnect attempts in the test transport, with browser HTTP offline
|
||
emulation too. Draft survives, Send is disabled, reconnect restores the
|
||
composer in Chromium and WebKit. The first Chromium offline-only attempt
|
||
did not terminate the existing WebSocket and is recorded as test setup
|
||
failure, not an application bug.
|
||
- Dark settings rendered correctly; no page JavaScript errors were caught
|
||
during the successful full runs. Large system text/VoiceOver, real mobile
|
||
keyboard and OS-level media permissions remain untested.
|
||
- Browser sessions were sequential. Minimum sampled available RAM:
|
||
1,638 MiB in Chromium, 1,748 MiB in WebKit. WebKit one-minute load reached
|
||
3.73 on two CPUs; swap also varied slightly. Keep one worker and avoid
|
||
simultaneous builds. Long-run or production-latency guarantees were not
|
||
established. After tests nginx, Prosody, LLDAP and Traefik were active,
|
||
chat returned HTTPS 200, and no audit browser processes remained.
|
||
|
||
WebKit originally failed creating its WPE EGL display; pointing it at
|
||
Nix Mesa software-rendering libraries fixed the run without a NixOS switch.
|
||
The runner and runtime packages are documented below.
|
||
|
||
## Implementation batches
|
||
|
||
The detailed follow-up is [UX implementation plan](ux-implementation-plan.md):
|
||
ordered packages, source areas, compatibility decisions, regression checks
|
||
and release boundaries. The plan is proposed; no package is implemented yet.
|
||
|
||
1. **Direct-message reliability:** fresh device lists and truthful status
|
||
when decrypting fails; explain unsupported encrypted media before
|
||
selection/upload, then plan compatible encrypted attachments.
|
||
2. **Keep user state:** local per-conversation drafts and reading anchors;
|
||
cached-history access and session retry on offline startup.
|
||
Regression: navigating away and back retains both; new messages and
|
||
attachments do not pull a reader away from older content.
|
||
3. **Group info:** accessible header entry, available people/roles,
|
||
person actions, invite/leave and explicit full-list permission handling.
|
||
Test owner/member and an absent member, not only two active owners.
|
||
4. **Phone interaction:** quoted replies, larger touch targets, menu/modal
|
||
focus and dismissal; validate on the actual iPhone during implementation.
|
||
5. **Daily-use extensions:** contact picker, search and per-chat mute after
|
||
operator prioritization and checking protocol/client compatibility.
|
||
|
||
## Extended direct-chat and PWA simulation results
|
||
|
||
The operator requested simulating PWA use and expanding direct chats.
|
||
These checks were actually run; their level of simulation is explicit:
|
||
|
||
| Check | Chromium | Linux WebKit | What the result establishes |
|
||
| --- | --- | --- | --- |
|
||
| Contact request / roster | Request accepted in first extended run; restored later | Existing contact restored | Test accounts can become contacts; enabled call controls appeared. No audio/video call was placed. |
|
||
| Direct text, receipts, unread | Delivered → read; unread row/title cleared on opening | Same | Healthy path after the documented device-cache intervention. |
|
||
| Typing / multiline / links | Passed | Passed | Typing starts/ends, touch Enter inserts a newline, Unicode/link text arrives and links use a new tab with appropriate rel attributes. |
|
||
| Newly added device | Unreadable new live message; sender read tick | Same | Reproducible stale-device-list failure, distinct from old backlog. |
|
||
| Direct drafts / reading position | Drafts lost; reading distance 3,006 → 0 px | Drafts lost; extended position probe inconclusive (0 → 0) | Direct position gap confirmed in Chromium; earlier group-position checks passed reproduction in both engines. |
|
||
| Direct image/file/voice | Image/file rejected; synthetic voice recording/discard works, send rejected | Image/file rejected; voice not run | Missing encrypted attachment capability. |
|
||
| Standalone indicators | Injected, shell controlled, assets cached | Injected, shell controlled; page cache inventory empty | App renders with the simulated indicators. This does not install a PWA. WebKit cache result needs engine/device validation. |
|
||
| Keyboard-size viewport | Composer reachable at 402 × 500 and 874 × 300 | Same | Resizing the layout viewport works; native keyboard overlays and safe-area values are not reproduced. |
|
||
| Background-like freeze/resume | Draft retained; one incoming message after brief freeze | Not run | Chromium lifecycle freeze works in this short scenario, not iOS process eviction or long background suspension. |
|
||
| Worker push / notification click | Notification created; route opens correct chat; focus denied for untrusted click | Not run | Real worker executes injected push and click handlers. It does not prove OS delivery, OS focus or opening a closed iPhone PWA. |
|
||
| Worker update check | Passed, unchanged worker | Same | Update API works; no changed production version or old/new release migration was tested. |
|
||
| Persistent-profile restart | Saved login, browser/OMEMO device IDs and message retained; draft lost | Same | Real browser shutdown/relaunch preserves those stored states. |
|
||
| Offline cold start | Cached shell, history hidden, no automatic recovery in 10 seconds; reload restores session | Internal navigation error | Chromium application gap above; WebKit result is an execution/environment limitation, not a proven Safari bug. |
|
||
|
||
Chromium push uses CDP `ServiceWorker.deliverPushMessage`. The click is an
|
||
untrusted `NotificationEvent` dispatched to the real worker with its
|
||
`waitUntil` completion captured. Posting the chat route succeeds; `focus()`
|
||
rejects with "Not allowed to focus a window." That expected synthetic-click
|
||
restriction is not claimed as an application defect. Playwright persistent
|
||
contexts preserve storage across actual browser restarts.
|
||
[Chromium ServiceWorker protocol](https://chromedevtools.github.io/devtools-protocol/tot/ServiceWorker/),
|
||
[Playwright persistent contexts](https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context).
|
||
|
||
The first extended Chromium attempt used arrival assumptions that became
|
||
invalid because of the new-device failure, and later aborted before the
|
||
restart checks. A second run established the explicit cache intervention,
|
||
but its offline test treated failed automatic recovery as a timeout. The
|
||
final report linked above records offline state, the ten-second recovery
|
||
window and successful reload separately. Those earlier execution failures
|
||
are retained as evidence, not converted into successful checks.
|
||
|
||
No page JavaScript errors were reported in the final extended runs.
|
||
Minimum sampled available memory was 1,970 MiB in Chromium and 1,781 MiB
|
||
in WebKit. All runs were sequential, with a single browser process tree
|
||
and isolated account contexts. This remains a short-run resource result.
|
||
Maximum sampled one-minute load was 2.13 in Chromium and 2.36 in WebKit.
|
||
After the extended runs, nginx/Prosody/LLDAP/Traefik were active, chat HTTPS
|
||
returned 200 in 0.094 seconds, and the host process inventory contained no
|
||
remaining audit browser processes. This is an end-of-run health check,
|
||
not continuous production-latency monitoring.
|
||
Credentials, caches and keys are in the private runtime profiles, not Git.
|
||
|
||
Remaining device-only validation is narrower: actual iOS keyboard/visual
|
||
viewport and safe areas, Home Screen installation, OS background/eviction,
|
||
real push delivery and trusted notification opening, media permissions and
|
||
hardware, large text/VoiceOver. The simulation results above already cover
|
||
the app behavior that could be tested autonomously on blunix.
|
||
|
||
## Real iPhone PWA result sheet — pending
|
||
|
||
Use the existing installed PWA on iPhone 17 Pro / iOS 26.6. Do not clear its
|
||
data or log out just to run the audit. Record pass/fail and short notes:
|
||
|
||
| ID | Device task | What to observe | Result |
|
||
| --- | --- | --- | --- |
|
||
| PWA01 | Open a chat, type harmless unsent multiline text, open/close keyboard | Composer/latest message reachable; no hidden buttons or jump | Pending |
|
||
| PWA02 | Navigate between chat/list with a harmless draft and while reading old messages | Confirm known draft/reading-position gaps on real PWA | Pending |
|
||
| PWA03 | Switch apps and lock/unlock, then reopen chat | Session and composer recover predictably | Pending |
|
||
| PWA04 | Toggle connectivity, then restore it | Clear reconnect state; draft kept; no duplicate sending | Pending |
|
||
| PWA05 | Receive a message from an arranged tester while PWA is backgrounded/closed; open notification | Correct chat, unread count and badge | Pending; requires arranged sender |
|
||
| PWA06 | Photo/file selection and voice recording, using arranged test chat | Permission prompt, cancel/send and playback work | Pending; requires test data |
|
||
| PWA07 | Large text, VoiceOver, safe areas and orientation | Readable/reachable controls; no clipping; focus behavior | Pending |
|
||
|
||
Notification delivery here cannot be certified by Linux WebKit. iOS web
|
||
push applies to Home Screen web apps; actual permission, background and
|
||
notification-open behavior needs the device. [Apple/WebKit](https://webkit.org/blog/13878/web-push-for-web-apps-on-ios-and-ipados/).
|
||
|
||
## Re-running the audit
|
||
|
||
`ux-audit-runner.cjs` records gaps and measurements without treating an
|
||
observed UX gap as a script failure; execution failures are explicit.
|
||
It uses the existing dedicated test room and requires its accounts to have
|
||
membership. Passwords remain in the mode-0600 runtime accounts file.
|
||
|
||
```bash
|
||
npm install --prefix /tmp/chat-ux-runner --no-audit --no-fund playwright
|
||
node ux-audit-runner.cjs chromium
|
||
node ux-audit-runner.cjs chromium --reconnect-only
|
||
node ux-pwa-direct-runner.cjs chromium
|
||
|
||
npm install --prefix /tmp/chat-ux-webkit --no-audit --no-fund playwright@1.61.1
|
||
env UX_PLAYWRIGHT_MODULE=/tmp/chat-ux-webkit/node_modules/playwright \
|
||
UX_WEBKIT_PATH=/nix/store/kvbchbp55jd09wh2h1bv3xx9cz26k6vg-playwright-webkit/pw_run.sh \
|
||
LIBGL_ALWAYS_SOFTWARE=1 EGL_PLATFORM=surfaceless \
|
||
LIBGL_DRIVERS_PATH=/nix/store/ppphyhp48gzdji3q9q945gmamy4rrj3f-mesa-26.2.2/lib/dri \
|
||
__EGL_VENDOR_LIBRARY_FILENAMES=/nix/store/ppphyhp48gzdji3q9q945gmamy4rrj3f-mesa-26.2.2/share/glvnd/egl_vendor.d/50_mesa.json \
|
||
node ux-audit-runner.cjs webkit
|
||
```
|
||
|
||
For extended WebKit checks, use the same environment command with
|
||
`node ux-pwa-direct-runner.cjs webkit` instead. That runner creates a private
|
||
profile in its per-run results directory; do not export/commit that profile.
|
||
It changes only the synthetic accounts' contact relationship and sends
|
||
synthetic messages/uploads. Its cache invalidation is restricted to the
|
||
private A profile and is always recorded as `TEST-CACHE-REFRESH`.
|
||
|
||
Nix browser/Mesa paths refer to the packages fetched for this audit; rebuild
|
||
the corresponding package attributes if they were garbage-collected.
|
||
The app package and its package-lock were not changed for browser tooling.
|
||
|
||
## Implementation follow-up — 2026-10-07
|
||
|
||
Packages 0–3 are implemented and released. See [implementation results](UX-IMPLEMENTATION-RESULTS.md) for candidate/deployment evidence, the corrected WebKit scroll setup and native validation limits. The findings above describe the original audited build and remain historical evidence.
|