Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
a6d204f
01a0234b - fix(buy): show and send exact CHF with rappen (#926)
TaprootFreak Aug 21, 2026
8321754
Merge pull request #927 from RealUnitCH/staging
TaprootFreak Aug 21, 2026
568f6a4
01a02915 - Live RealUnit geo-filter table in the handbook (#930)
TaprootFreak Aug 22, 2026
f491ac0
01a02915 - Geo-filter DE/EN names and downloads (#932)
TaprootFreak Aug 22, 2026
519bdbb
fix(buy): keep the amount field editable — quote the Rappen-exact pay…
Blume1977 Aug 24, 2026
81aaf68
Merge pull request #931 from RealUnitCH/staging
TaprootFreak Aug 24, 2026
a438df3
01a03350 - Show exact buy charge and skip duplicate Maestro on ready …
TaprootFreak Aug 24, 2026
42dea71
01a033fb - test(buy): pin EUR quotes to the EUR settlement IBAN (#938)
TaprootFreak Aug 24, 2026
4a71a59
01a0344d - fix(buy): hide FormatException on 502 and pin retry-only g…
TaprootFreak Aug 24, 2026
eeb8812
01a033b5 - Buy CTA: quote max amount and thousands grouping (#936)
TaprootFreak Aug 24, 2026
892f909
01a033fb - Default display currency from account residence, not local…
TaprootFreak Aug 25, 2026
9e75d18
Merge pull request #940 from RealUnitCH/staging
TaprootFreak Aug 25, 2026
cc96b44
01a03ced - Delay send-process start until the route slide finishes (#…
TaprootFreakAI Aug 26, 2026
d0174ce
01a03ced - Wait for incoming slides before starting process work (#946)
TaprootFreakAI Aug 26, 2026
05a4a50
Merge pull request #945 from RealUnitCH/staging
TaprootFreak Aug 26, 2026
172f329
01a03dfc - Send: receipt timeout is success, no raw viem (#947)
TaprootFreakAI Aug 26, 2026
cd4cbe9
01a03e98 - Don't double-push send/pay after a live QR scan (#948)
TaprootFreakAI Aug 26, 2026
ce58eee
Merge pull request #949 from RealUnitCH/staging
TaprootFreak Aug 26, 2026
31fc2fc
01a03951 - BitBox sell: faucet then unsigned swap and deposit (#950)
TaprootFreakAI Aug 27, 2026
19ca0a1
01a042cc - Golden for the collapsed all-zero portfolio chart (#953)
TaprootFreakAI Aug 27, 2026
6f75fe5
01a042cc - Keep portfolio chart visible when holdings are zero (#951)
TaprootFreakAI Aug 27, 2026
b7841e9
01a042cc - Handbook test-customer portfolio charts (#954)
TaprootFreakAI Aug 28, 2026
209c85a
01a042cc - Bump handbook mail-preview count to 28 (#956)
TaprootFreakAI Aug 28, 2026
ce45560
Merge pull request #952 from RealUnitCH/staging
TaprootFreak Aug 28, 2026
d5b760b
01a04768 - Move Send to the receive QR screen and add send/pay info (…
TaprootFreakAI Aug 28, 2026
222aaef
Merge pull request #957 from RealUnitCH/staging
TaprootFreak Aug 28, 2026
e7ccd98
01a04768 - Retry Maestro driver hangs and freeze handbook persona dat…
TaprootFreakAI Aug 29, 2026
ecc5131
Merge pull request #959 from RealUnitCH/staging
TaprootFreak Aug 29, 2026
8fb18a2
01a04768 - Retry Sentry symbol upload on store deploy (#960)
TaprootFreakAI Aug 29, 2026
1c4cf03
Merge pull request #961 from RealUnitCH/staging
TaprootFreak Aug 29, 2026
36a4c16
01a04768 - Do not block Play Store on stuck Sentry assemble (#962)
TaprootFreakAI Aug 30, 2026
fd01f27
Merge pull request #963 from RealUnitCH/staging
TaprootFreak Aug 31, 2026
616ebec
01a0672f - test(history): pin inbound transfers labelled as Kauf (#967)
TaprootFreakAI Sep 7, 2026
90fb9ed
01a0672f - feat(history): label plain REALU transfers as received/sen…
TaprootFreakAI Sep 7, 2026
7ee384c
01a03535 - Add referral programme and promo-code client (#966)
JonnyLuca Sep 8, 2026
594a627
Merge pull request #968 from RealUnitCH/staging
TaprootFreak Sep 8, 2026
9b4a5ca
01a07ca9 - Handbook Geo-Filter nur ja oder nein (#970)
TaprootFreakAI Sep 8, 2026
0a7e9a7
01a082d4 - Hide tax countries the backend rejects on taxEnable (#972)
TaprootFreakAI Sep 9, 2026
535af35
01a086de - Accept referral terms with PUT and bundle the 2026-08-26 T…
Blume1977 Sep 10, 2026
40332dc
01a07ca9 - PNG logo for RealUnit mails (#973)
TaprootFreakAI Sep 10, 2026
0faa1d3
01a08940 - Document the 200 REALU referral first-buy minimum in the h…
TaprootFreakAI Sep 10, 2026
ee8944e
01a08940 - Bump handbook mail-preview count to 26 (#977)
TaprootFreakAI Sep 10, 2026
745b114
01a08940 - Stage invite/promo web baselines (36 → 146) (#978)
TaprootFreakAI Sep 10, 2026
9915b94
01a08960 - Load referral terms from in-app Markdown like other legal …
TaprootFreakAI Sep 10, 2026
853e0f2
01a08bdf - fix(ui): render AppBar titles and highlighted descriptions…
TaprootFreakAI Sep 11, 2026
3f82b96
01a08bdf - docs(handbook): align prose and captions with the app UI, …
TaprootFreakAI Sep 11, 2026
6aa948f
Merge pull request #971 from RealUnitCH/staging
TaprootFreak Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions .coverage-visibility-allowlist
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# Abstract interfaces / ports (implementations live elsewhere and are tested):
lib/packages/io/backup_exclusion_port.dart
lib/packages/io/documents_directory_port.dart
lib/packages/io/install_referrer_port.dart
lib/packages/service/biometric/biometric_port.dart
lib/packages/service/price_service.dart
lib/screens/kyc/steps/ident/cubits/kyc_ident/sumsub_ident_port.dart
Expand Down
29 changes: 15 additions & 14 deletions .github/workflows/handbook-build-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ name: Handbook Build Check
# - scripts/assemble-handbook-screenshots.sh handbook→Golden mapping
# - test/goldens/** Golden baselines (source of every
# screenshot the handbook serves)
# - scripts/assemble-handbook-store-listing.py, scripts/assemble-handbook-legal.py
# store-listing / legal-downloads generators
# - ios/fastlane/**, android/fastlane/metadata/**, assets/legal/**, assets/languages/**
# store texts, screenshots and legal documents
# - Dockerfile.handbook multi-stage build
# - handbook.nginx.conf nginx config
# - handbook.htpasswd access gate
Expand Down Expand Up @@ -92,8 +96,8 @@ jobs:
set -euo pipefail
bash scripts/assemble-handbook-screenshots.sh /tmp/handbook-shots
count=$(ls -1 /tmp/handbook-shots/*.png | wc -l | tr -d ' ')
if [ "$count" != "278" ]; then
echo "expected 278 screenshots, got $count" >&2
if [ "$count" != "299" ]; then
echo "expected 299 screenshots, got $count" >&2
exit 1
fi

Expand Down Expand Up @@ -173,15 +177,13 @@ jobs:
exit 1
fi

# Screenshots dir must contain all 278 PNGs assembled from Goldens.
# Hit one of them through the auth gate to verify wiring end-to-end.
# Mix of the original 01-61 range and the 62-269 batch (every Golden
# baseline) so a regression in either half surfaces here.
for name in 01-welcome 11-dashboard 26-terms 35-dashboard-with-balance 46-buy-kyc-required 52-sell-unknown-error 53-buy-payment-details 61-kyc-registration-tax-tin-error 62-welcome-page-android 219-settings-security-page-default 268-phone-number-field-default 269-dashboard-insider-unlocked; do
code=$(curl -s -o /dev/null -w '%{http_code}' -u "${HANDBOOK_USER:-x}:${HANDBOOK_PASS:-x}" "http://127.0.0.1:8080/screenshots/${name}.png")
# 200 (auth happens to match) or 401 (auth fails but file exists)
# both prove the file is on disk. 404 means it was not assembled.
if [ "$code" = "404" ]; then
# Spot-check handbook screenshots assembled from Goldens (299 PNGs).
# Mix of original 01-61 slots, later Golden batches, and referral
# baselines so a regression in any range surfaces here.
for name in 01-welcome 11-dashboard 26-terms 35-dashboard-with-balance 46-buy-kyc-required 52-sell-unknown-error 53-buy-payment-details 61-kyc-registration-tax-tin-error 62-welcome-page-android 219-settings-security-page-default 268-phone-number-field-default 269-dashboard-insider-unlocked 270-handbook-persona-dca 275-handbook-persona-exit-1j 276-referral-overview 282-referral-payout-row 283-referral-terms-readonly 287-kyc-registration-referral-recognized 288-transaction-history-referral-payout 289-dashboard-referral-entry-and-payout 290-kyc-registration-referral-promo; do
# Auth is checked before the file exists: a missing PNG also
# returns 401, so HTTP status cannot prove assembly. Look on disk.
if ! docker exec handbook test -f "/usr/share/nginx/html/screenshots/${name}.png"; then
echo "screenshot ${name}.png missing from /usr/share/nginx/html/screenshots/" >&2
docker logs handbook
exit 1
Expand All @@ -191,9 +193,8 @@ jobs:
# Legal downloads must be present (built by the legal-docs-builder
# stage via pandoc). Same 200/401-vs-404 logic: 404 means the PDF/DOCX
# was not produced. Covers PDF + DOCX and both languages.
for f in legal/privacy_policy_de.pdf legal/privacy_policy_de.docx legal/terms_of_use_en.pdf legal/registration_agreement_de.docx; do
code=$(curl -s -o /dev/null -w '%{http_code}' -u "${HANDBOOK_USER:-x}:${HANDBOOK_PASS:-x}" "http://127.0.0.1:8080/${f}")
if [ "$code" = "404" ]; then
for f in legal/privacy_policy_de.pdf legal/privacy_policy_de.docx legal/terms_of_use_en.pdf legal/registration_agreement_de.docx legal/referral_terms_de.pdf legal/referral_terms_en.pdf legal/referral_terms_de.docx legal/referral_terms_en.docx; do
if ! docker exec handbook test -f "/usr/share/nginx/html/${f}"; then
echo "legal download ${f} missing from /usr/share/nginx/html/legal/" >&2
docker logs handbook
exit 1
Expand Down
29 changes: 15 additions & 14 deletions .github/workflows/handbook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -121,19 +121,20 @@ jobs:
env:
API_DIR: _api-checkout
HANDLEBARS_PREFIX: _handlebars-only
# Exact expected mail-preview file count: 24 mails + 1 index = 25.
# `-ne` below makes ANY drift (drop OR addition) fail the build —
# adding a new mail upstream therefore requires an explicit edit
# here, which is the whole point: a silently-added (or silently-
# removed) mail must never reach the handbook image undetected.
# When you legitimately add/remove a mail in
# DFXswiss/api/scripts/generate-realunit-previews.js, bump this
# number in the SAME PR as the api-side change-PR (or its
# follow-up). The exact count is computed as:
# N(add() calls in the generator)
# Exact expected mail-preview file count: 22 standalone add() mails
# + 3 pendingTypes expansions + 1 index = 26. `-ne` below makes ANY
# drift (drop OR addition) fail the build — adding a new mail
# upstream therefore requires an explicit edit here, which is the
# whole point: a silently-added (or silently-removed) mail must
# never reach the handbook image undetected. When you legitimately
# add/remove a mail in DFXswiss/api/scripts/generate-realunit-previews.js,
# bump this number in the SAME PR as the api-side change-PR (or
# its follow-up). The exact count is computed as:
# N(standalone add() calls in the generator; the loop template
# is not counted here)
# + N(pendingTypes entries the for-loop expands)
# + 1 (00_index.html)
EXPECTED_HTML_COUNT: 25
EXPECTED_HTML_COUNT: 26
run: |
set -euo pipefail

Expand Down Expand Up @@ -278,7 +279,7 @@ jobs:

# Like the receipt examples, these PDFs are already committed in the api
# repo (rendered by BalancePdfService via the api-side spec
# realunit-balance-example.spec.ts), so no generator / npm run is needed
# realunit-statement-example.spec.ts), so no generator / npm run is needed
# here — we only copy them into the build context. Reuses the same
# _api-checkout/ from the mail step above. NOTE the SEPARATE dest dir
# (docs/handbook/balance/, not receipts/) so the receipt step's
Expand Down Expand Up @@ -328,13 +329,13 @@ jobs:
- name: Stage web e2e baselines from web repo
env:
WEB_DIR: _web-checkout
# RealUnitCH/web commits 36 Playwright visual baselines under
# RealUnitCH/web commits 146 Playwright visual baselines under
# tests/__screenshots__/{desktop-chromium,tablet-chromium,mobile-safari}/.
# `-ne` below fails the build on ANY drift (add OR remove) so a change
# to the committed set upstream requires an explicit bump here AND a
# matching card edit in docs/handbook/de/index.html (#spec-web) in the
# SAME change — same guard rationale as the api steps above.
EXPECTED_WEB_BASELINE_COUNT: 36
EXPECTED_WEB_BASELINE_COUNT: 146
run: |
set -euo pipefail

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ jobs:
- run: dart run tool/generate_localization.dart
- run: dart run tool/generate_release_info.dart
- run: flutter pub run build_runner build
- run: bash scripts/run-handbook-flows.sh --matcher-self-test
- run: flutter analyze
# Excludes the `golden` tag: visual-regression tests live under
# `test/goldens/` and are validated on the self-hosted runner
Expand Down
41 changes: 32 additions & 9 deletions .github/workflows/tier3-handbook.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,24 @@ name: Tier 3 — Handbook flows
# the existing Analyze & Test job. Reviewers opt-in by labelling
# the PR. The label gate is the cost control, not the branch
# filter — stacked PRs against integration branches need the same
# opt-in option.
# opt-in option. Types are `opened` / `synchronize` / `reopened` /
# `labeled` / `unlabeled`. `ready_for_review` is omitted on purpose:
# a labelled draft that is later marked ready must not start a
# second macOS run — the labelled run already covers that SHA.
# (Contrast `pull-request.yaml`, where draft-skip means becoming
# ready is the first real run and `ready_for_review` belongs there.)
# * `push: develop` always runs — post-merge verification is the
# authoritative source of truth for "did the handbook flows survive
# this merge", independent of any PR state. Same reasoning as
# `pull-request.yaml`.
# * `workflow_dispatch` for manual catch-up runs and a re-run path when
# the macOS image flips an Xcode default and we need to verify the fix
# before re-arming the gate. Always runs (label gate doesn't apply).
# * `labeled` / `unlabeled` keep the trigger surface in sync with the
# `tier3:full` opt-in: adding the label after the PR opened starts a
# run; removing it cancels via the concurrency block below.
# * `labeled` / `unlabeled` stay in `types` so GitHub delivers the
# opt-in events. Adding `tier3:full` starts a run (job `if` matches
# that label name). Removing it does not start a run and does not
# cancel an in-flight one — labeled events use a unique concurrency
# group, so an unlabeled skip cannot stop the flow already running.
#
# Concurrency:
# Same pattern as `pull-request.yaml`: group by `pull_request.number`
Expand All @@ -36,7 +43,10 @@ name: Tier 3 — Handbook flows
# get their own `ci-<workflow>-label-<run_id>` group so a label toggle
# does NOT kill an already-running ~15-minute flow execution — the user
# experience of "I added the label, the previous run died" is worse
# than the cost of one extra runner-minute.
# than the cost of one extra runner-minute. That unique group is also
# why omitting `ready_for_review` matters: a later ready event would
# join the PR-number group and would not cancel the in-flight labeled
# run, so both would race on macos-latest.
#
# Locale: scripts/run-handbook-flows.sh pins the booted simulator to de_CH
# so German handbook assertions pass on `macos-latest` runners (default
Expand Down Expand Up @@ -66,8 +76,11 @@ name: Tier 3 — Handbook flows
# as a safety net for that residual crash class, retrying the
# driver hang/death class against both the CLI tee-log and
# `--debug-output` maestro.log (`IOSDriverTimeoutException`,
# ConnectException on :7001, Connection refused/reset) —
# assertion failures without those patterns are never retried.
# ConnectException on :7001, Connection refused/reset,
# UnknownFailure HTTP 500 on :7001/deviceInfo). A hung JVM after
# that 500 is killed per attempt (`MAESTRO_ATTEMPT_TIMEOUT_SEC`)
# so the 60-minute job does not sit until GitHub cancels it.
# Assertion failures without those patterns are never retried.
# The post-mortem block (lsof / ps / simctl log + Maestro
# `--debug-output`) captures failure state so future regressions
# can be diagnosed forensically. The CI-hardening track that landed
Expand Down Expand Up @@ -110,7 +123,7 @@ on:
# covered by `push: develop` on the same SHA — see the longer
# rationale in `pull-request.yaml`).
branches-ignore: [main]
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
types: [opened, synchronize, reopened, labeled, unlabeled]

concurrency:
group: >-
Expand All @@ -131,9 +144,19 @@ jobs:
# Drafts pass through unconditionally on push (they cannot push, so
# the only way here is via a labelled PR) — no extra `draft == false`
# guard needed.
# `labeled` must match the opt-in label itself: with only `contains(...)`,
# adding any other label on an already-labelled PR would start a second
# macos-latest run (labeled events use a unique concurrency group and
# would not cancel the in-flight one). `unlabeled` is skipped here —
# the unique group also means it cannot cancel an in-flight flow.
if: >-
github.event_name != 'pull_request'
|| contains(github.event.pull_request.labels.*.name, 'tier3:full')
|| (github.event.action == 'labeled' && github.event.label.name == 'tier3:full')
|| (
github.event.action != 'labeled'
&& github.event.action != 'unlabeled'
&& contains(github.event.pull_request.labels.*.name, 'tier3:full')
)
runs-on: macos-latest
# ~12 min setup (Flutter + tooling, build, sim erase/boot/install) plus
# ~26-33 min for the 26 handbook flows — each flow restarts the XCUITest
Expand Down
6 changes: 6 additions & 0 deletions .maestro/handbook/11-dashboard.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ appId: swiss.realunit.app
- extendedWaitUntil:
visible: 'RealUnit kaufen'
timeout: 30000
# Fresh handbook wallet is not a verified shareholder with 70 REALU
# (TB Ziff. 2, analog Verkaufen). The dashboard card must stay off this
# screen; 289-dashboard-referral-entry-and-payout is golden-only.
- extendedWaitUntil:
notVisible: 'Erhalte 20 REALU pro Weiterempfehlung'
timeout: 10000
10 changes: 10 additions & 0 deletions .maestro/handbook/12-settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,13 @@ appId: swiss.realunit.app
- extendedWaitUntil:
visible: 'Einstellungen'
timeout: 30000
# Same gate as the dashboard card. Wait for a settings tile that does not
# depend on the referral API, then assert Empfehlungen is still absent so
# a late summary 200 cannot have opened the tile on a fresh wallet.
- extendedWaitUntil:
visible:
text: '.*Rechtsdokumente.*'
timeout: 15000
- extendedWaitUntil:
notVisible: 'Empfehlungen'
timeout: 8000
17 changes: 17 additions & 0 deletions .maestro/handbook/20-settings-legal-documents.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,20 @@ appId: swiss.realunit.app
visible:
text: '.*Dokumente der Aktionariat AG.*'
timeout: 30000
# Last tile is the 14.08 Teilnahmebedingungen (Ziff. 2–11). On a phone it can
# sit below the fold, so scroll it into view, then restore Aktionariat so
# flow 21 can tap that entry.
- scrollUntilVisible:
element:
text: '.*Teilnahmebedingungen Referral-Programm.*'
direction: DOWN
timeout: 15000
- extendedWaitUntil:
visible:
text: '.*Teilnahmebedingungen Referral-Programm.*'
timeout: 10000
- scrollUntilVisible:
element:
text: '.*Dokumente der Aktionariat AG.*'
direction: UP
timeout: 15000
21 changes: 20 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ The app must stay fully usable on every **standard phone** we support (smallest

New sticky-CTA UI without `ScrollableActionsLayout` + matrix entry is a **blocking** review finding.

## Scanner navigation — CRITICAL

[`QrScannerView`](lib/widgets/scanner/qr_scanner_view.dart) / `MobileScanner` forwards **every camera frame** while a barcode is in view — it is not a one-shot capture.

**Navigation rule**

- Navigating from a scan result in the same turn as `cubit.reset()` is **forbidden**: it drops the cubit's "already decoded" / Valid guard, so the next frame pushes a second copy of the next route. The user ends up on two stacked amount (or quote) screens and cannot leave cleanly.
- Production contract: every `QrScannerView` consumer uses [`pushThenRearm`](lib/widgets/scanner/push_then_rearm.dart) to push the next route. Rearm / `reset` runs after `Route.completed` (the outgoing animation is gone) or immediately if the push throws — **never** in the same listener turn. In this app a scanner that does not push a route is not a valid consumer — add it to the catalog with `pushThenRearm` or do not construct `QrScannerView`.
- Cubit `onCodeDetected` must ignore further detections while the decoded/valid state is held. That guard is **necessary and not sufficient** — without `pushThenRearm`, an early `reset()` makes it a no-op.

**Test rule (gate for this bug class)**

- Catalog: [`test/helper/scanner_navigation_catalog.dart`](test/helper/scanner_navigation_catalog.dart).
- Self-test: [`scanner_navigation_catalog_test.dart`](test/helper/scanner_navigation_catalog_test.dart) — every catalogued production + regression path exists; production files contain `QrScannerView(` **and** `pushThenRearm(`; regression files fire `BarcodeCapture` and assert the destination `findsOne`; **discovery** walks `lib/` and fails if any `QrScannerView(` consumer is missing from the catalog (skipping the widget definition file itself).
- Each surface has a real-cubit widget test that fires **two** `BarcodeCapture`s and expects the destination `findsOne`, then pop, then a third capture is accepted again. Mocking the cubit / `whenListen` of a single Valid/Decoded state cannot catch this bug class.
- Details and PR checklist: [`docs/testing.md`](docs/testing.md) § *Scanner navigation*.

New `QrScannerView` consumer without catalog entry + `pushThenRearm` + double-capture test is a **blocking** review finding.

## Branch Flow

Three branches participate in the release lane:
Expand Down Expand Up @@ -229,7 +248,7 @@ The app supports three wallet modes (`software`, `bitbox`, `debug`) with differe
```bash
rg "^//\s*@no-integration-test:" lib/
```
- Visual-regression Goldens under `test/goldens/screens/` are also the source of the 26 screenshots served at `handbook.realunit.app`. When you add a handbook page, you MUST add a matching Golden test AND a row in the mapping table at `scripts/assemble-handbook-screenshots.sh` — the handbook will not pick up a Maestro-captured PNG anymore. The `Handbook Build Check` workflow on every PR runs the assembly script and fails loudly if a mapped Golden is missing.
- Visual-regression Goldens under `test/goldens/screens/` and `test/goldens/widgets/` are also the source of the 299 screenshots served at `handbook.realunit.app`. When you add a handbook page, you MUST add a matching Golden test AND a row in the mapping table at `scripts/assemble-handbook-screenshots.sh` — the handbook will not pick up a Maestro-captured PNG anymore. The `Handbook Build Check` workflow on every PR runs the assembly script and fails loudly if a mapped Golden is missing.
- Why: single source of truth — a UI regression that breaks a Golden also breaks the handbook image before either ships; eliminates the previous "two pipelines, two truths" problem.
- See: [`docs/visual-regression-tests.md`](docs/visual-regression-tests.md) section "Handbook screenshots are sourced from Goldens".

Expand Down
Loading
Loading