Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
c23246f
Make the shell fit a phone rather than a picture of one
bitcoinuniverseadmin Aug 29, 2026
63d1892
Run the mobile gate beside the matrix, and write the model down
bitcoinuniverseadmin Aug 29, 2026
7976cea
The scroll shadow is a token, because its weight is a measurement
bitcoinuniverseadmin Aug 29, 2026
c49b4e3
Cover the three pieces of the shell that are not CSS
bitcoinuniverseadmin Aug 29, 2026
4a9614b
Move the bar's own offset, not every ancestor's
bitcoinuniverseadmin Aug 29, 2026
1ec00a2
Three engines, and stop measuring the page mid-layout
bitcoinuniverseadmin Aug 29, 2026
cc42c7e
Measure what the shell costs a phone, against develop rather than a g…
bitcoinuniverseadmin Aug 29, 2026
2903b45
Every release checks the shell on a phone, then somebody looks at it
bitcoinuniverseadmin Aug 29, 2026
2bbead7
WebKit needs its system libraries, not just its binary
bitcoinuniverseadmin Aug 29, 2026
9f2e736
Name the focusable selector list once
bitcoinuniverseadmin Aug 29, 2026
eac4ef7
Run WebKit where it can actually start, and clear the Sass warnings
bitcoinuniverseadmin Aug 30, 2026
0e8a1b7
Merge develop: the chain page release-identity work
bitcoinuniverseadmin Aug 30, 2026
31f7a20
A red check must not blind the check after it
bitcoinuniverseadmin Aug 30, 2026
fca66c9
Merge pull request #21 from bitcoinuniverseio/design/mobile-adaptive-…
bitcoinuniverseadmin Aug 30, 2026
6c3dc82
Merge pull request #24 from bitcoinuniverseio/fix/chain-page-gate-run…
bitcoinuniverseadmin Aug 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions .github/workflows/universe-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -286,3 +286,189 @@ jobs:
scripts/universe/visual-qa/artifacts-broad
scripts/universe/visual-qa/artifacts-chain-states
retention-days: 30

# The mobile gate runs beside the visual matrix rather than inside it.
#
# The matrix job is already 60 minutes of its 100 minute cap, and 78 percent
# of that is the deep pass. Adding widths, orientations and device emulation
# to it is how a gate stops finishing, which is the fault the split between
# the broad and deep passes exists to prevent. So this is a job of its own on
# a second runner, and the wall clock of the workflow does not move: it
# finishes in roughly a fifth of the time the matrix takes, on hardware that
# would otherwise be idle while the matrix runs.
#
# It builds its own copy rather than taking an artifact from the job beside
# it. That costs one extra production build and buys two things worth more
# than it: the two gates cannot make each other fail or wait, and this one
# can be read, run and debugged on its own.
#
# Measured at about nine minutes, of which six are the build.
mobile:
name: Mobile and adaptive layout
runs-on: [self-hosted, linux-ultra]
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '24.19.0'
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Install
working-directory: frontend
run: npm ci

- name: Production build
working-directory: frontend
run: npm run build:universe

- name: Install the visual QA harness
working-directory: scripts/universe/visual-qa
run: npm ci

# Same reasoning as the matrix job: a fixed port once measured another
# job's build, so each run takes a free one and refuses to continue
# unless the process answering is the one it started.
- name: Serve the built frontend
run: |
GATEWAY_PORT=$(node -e "const s=require('net').createServer();s.listen(0,'127.0.0.1',()=>{console.log(s.address().port);s.close()})")
echo "GATEWAY_PORT=$GATEWAY_PORT" >> "$GITHUB_ENV"
UNIVERSE_GATEWAY_PORT=$GATEWAY_PORT UNIVERSE_GATEWAY_ROOT=frontend/dist/mempool/browser node scripts/universe/gateway.mjs &
GATEWAY_PID=$!
for i in $(seq 1 30); do
curl -fsS -o /dev/null "http://127.0.0.1:$GATEWAY_PORT/" && break
sleep 1
done
kill -0 "$GATEWAY_PID"
curl -fsS "http://127.0.0.1:$GATEWAY_PORT/__gateway/health"

# Eleven routes across seven window sizes, with a coarse pointer, a
# software keyboard, a display cutout and a rotation. Seventy-seven page
# loads, and every one of them is measured about twelve different ways,
# which is why this is cheap: the assertions are free and only the page
# loads cost anything.
- name: Every window a phone can be, measured with a thumb
working-directory: scripts/universe/visual-qa
run: node mobile-check.mjs --base=http://127.0.0.1:$GATEWAY_PORT --out=./artifacts-mobile

# What the shell costs a phone, and what it makes the page do while it
# arrives. The gated numbers are the compressed payload, which is a
# property of the build and identical on any machine, and layout shift,
# which is a property of the stylesheet. Largest paint is printed and not
# gated: this runner builds, serves and drives browsers at once, and the
# same commit measured twelve seconds and twenty on two routes whose
# shells are identical, so it would fail honest changes and pass slow
# ones depending on what else was running.
- name: What the shell costs on a phone
working-directory: scripts/universe/visual-qa
run: >-
node mobile-perf.mjs --base=http://127.0.0.1:$GATEWAY_PORT
--out=./artifacts-perf

- name: Keep the mobile report
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: universe-mobile-qa
path: |
scripts/universe/visual-qa/artifacts-mobile
scripts/universe/visual-qa/artifacts-perf
retention-days: 30

# The two engines the fleet cannot run.
#
# WebKit needs a set of system libraries a Chromium-only host has never had,
# and it does not degrade without them: it refuses to launch and prints the
# list. Installing them is one apt command, and the fleet's runner user has
# no passwordless sudo, so `playwright install --with-deps` fails there with
# "sudo: a password is required". Measured on universe-linux-ultra-03.
#
# So this one job runs on a hosted runner, where that command works. It is
# the exception to using the fleet, and it is here rather than folded into
# the job above so the exception is visible and so a hosted outage cannot
# take the Chromium gate down with it.
#
# It is worth the exception. WebKit is what Safari and Chrome on iOS are both
# built on, and dvh, safe-area insets and the pointer media feature are
# exactly the rules where engines disagree: a Chromium-only run proves
# nothing about the platform half this work was written for.
#
# When the fleet's image gains those libraries, delete this job and put the
# two steps back in the one above, which is where they belong.
mobile-engines:
name: WebKit and Firefox
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4

- name: Setup Node
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '24.19.0'
cache: npm
cache-dependency-path: frontend/package-lock.json

- name: Install
working-directory: frontend
run: npm ci

- name: Production build
working-directory: frontend
run: npm run build:universe

- name: Install the visual QA harness
working-directory: scripts/universe/visual-qa
run: npm ci

- name: Fetch the engines and what they need to start
working-directory: scripts/universe/visual-qa
run: npx playwright install --with-deps webkit firefox

- name: Serve the built frontend
run: |
GATEWAY_PORT=$(node -e "const s=require('net').createServer();s.listen(0,'127.0.0.1',()=>{console.log(s.address().port);s.close()})")
echo "GATEWAY_PORT=$GATEWAY_PORT" >> "$GITHUB_ENV"
UNIVERSE_GATEWAY_PORT=$GATEWAY_PORT UNIVERSE_GATEWAY_ROOT=frontend/dist/mempool/browser node scripts/universe/gateway.mjs &
GATEWAY_PID=$!
for i in $(seq 1 30); do
curl -fsS -o /dev/null "http://127.0.0.1:$GATEWAY_PORT/" && break
sleep 1
done
kill -0 "$GATEWAY_PID"
curl -fsS "http://127.0.0.1:$GATEWAY_PORT/__gateway/health"

# The full window set, on the engine Safari is built on.
- name: Every window a phone can be, on WebKit
working-directory: scripts/universe/visual-qa
run: >-
node mobile-check.mjs --base=http://127.0.0.1:$GATEWAY_PORT
--browser=webkit --out=./artifacts-mobile-webkit

# Firefox cannot be put into mobile emulation, so it reports a fine
# pointer and the pointer-conditional rules do not apply to it. What it
# still measures is every window size, the overflow, the fixed layers,
# the safe areas and the rotation, which is worth having on a third
# engine. A subset of routes, because this is the smoke check and the two
# thorough passes are elsewhere.
- name: A third engine, on the checks that do not need a thumb
working-directory: scripts/universe/visual-qa
run: >-
node mobile-check.mjs --base=http://127.0.0.1:$GATEWAY_PORT
--browser=firefox --routes=home,tx,address,blocks,docs
--out=./artifacts-mobile-firefox

- name: Keep the engine reports
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: universe-mobile-engines
path: |
scripts/universe/visual-qa/artifacts-mobile-webkit
scripts/universe/visual-qa/artifacts-mobile-firefox
retention-days: 30
11 changes: 11 additions & 0 deletions .github/workflows/universe-production-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,26 @@ jobs:
# development" to the public, while every check here passed. The API was
# answering correctly the whole time; the origin was serving an older
# build than anything measured it.
# These run whatever the step above concluded. They answer a different
# question: that one asks whether the authorities behind the pages can
# answer, this one asks whether the pages tell the truth about it. A
# chain in an index recovery makes the first fail honestly, and on its
# first run that is exactly what happened: index-doge-tap could publish
# no checkpoint, the job stopped there, and the gate that exists to catch
# an obsolete dashboard on the origin was skipped. A red check must not
# blind the check after it. The job still fails if either fails.
- name: Install the visual QA harness
if: ${{ !cancelled() }}
working-directory: scripts/universe/visual-qa
run: npm ci

- name: The gate can still tell a bad page from a good one
if: ${{ !cancelled() }}
working-directory: scripts/universe/visual-qa
run: node --test chain-page-audit.test.mjs

- name: The public chain pages say what the chain documents say
if: ${{ !cancelled() }}
working-directory: scripts/universe/visual-qa
run: >-
node chain-page-smoke.mjs
Expand Down
28 changes: 27 additions & 1 deletion docs/operations/DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,33 @@ suite can see that.
coverage dimensions, no whole snapshot identifier in the primary interface
and the whole one filed under the technical details, next actions that
resolve, and the frontend commit this release expected.
8. Keep the previous release directory until the stability window closes.

8. Check the shell on a phone against the public origin, not only in CI.

```bash
node scripts/universe/visual-qa/mobile-check.mjs --base=https://explorer.bitcoinuniverse.io
```

It walks eleven routes across seven window sizes with a coarse pointer, a
simulated display cutout and a rotation, and answers every request from
fixtures, so what it measures is the deployed shell rather than the chain.
Add `--browser=webkit` for the engine Safari is built on.

This is worth running against production and not only against the build,
because the two things it is most likely to catch are things CI cannot see:
a gateway serving a stale `index.html`, which shows up immediately as the
viewport meta losing `viewport-fit=cover`, and a configuration difference
that changes which destinations the bottom bar carries.

Then look at it by hand on a phone. The gate is emulation, and emulation is
not a device: it cannot tell you whether a thumb reaches the bottom bar
one-handed, whether the software keyboard covers the result you were
reading, or whether the page zooms when you tap the search field. Open the
header and search, switch chain, search with the keyboard up, open a
transaction, scroll a table, rotate, go back, and watch a live update
arrive. Current Safari on iPhone and iPad, Chrome on Android, Samsung
Internet, and Chrome on iPhone.
9. Keep the previous release directory until the stability window closes.

Rolling back to a release from before the socket handover needs the port back,
because such a gateway opens 8099 itself and dies on bind while systemd holds
Expand Down
Loading
Loading