The screenshot pipeline behind fleetbase.io
Fleetbase
·
Documentation
·
Storefront app
·
Latest screenshots
Every product screenshot on fleetbase.io and in the Fleetbase documentation comes from here. On each Fleetbase release, this repository boots that release from its published Docker images, seeds a fictional demo company, captures the console in dark and light, and proposes only the screenshots that actually changed as a pull request to the website. The same seeded stack drives the Storefront app on an iOS Simulator and an Android Emulator.
It is published as a working reference: how to run Fleetbase from the official images, how to seed it with realistic data, and how to make screenshots of a web app reproducible enough to automate. It is not a supported product, and most people will want Fleetbase itself.
- Real releases, nothing built: every image is pinned to the release tag, so a screenshot shows exactly what that version ships.
- Fictional, deterministic data: fixed names, places and timestamps (a logistics company in Singapore), with no Faker and no random ids, so two runs produce the same pixels.
- Two gates before publishing: a DOM hash and a pixel comparison, so a pull request only carries screens that really changed.
- Dark and light: every screen in both themes, named by one rule (NAMING.md).
Browse the latest screenshots: the console, area by area, and the Storefront app on iOS and Android, each in dark and light and from the most recent run.
fleetbase/fleetbase release vX.Y.Z
└─ publishes fleetbase-console:vX.Y.Z and fleetbase-api:vX.Y.Z
└─ repository_dispatch → this repository
│
boot the published images behind one proxy on http://localhost
→ provision → seed → capture (Playwright) → compare → pull request on fleetbase.io
It lives apart from the main repository so that demo data and marketing tooling stay out of the product, and so the same pipeline can also capture extensions that are not part of the open-source monorepo.
You need Docker and Node 22. The console image is amd64-only, so on Apple silicon it runs under emulation (see caveats).
RELEASE_TAG=v0.7.74 ./scripts/up.sh./scripts/provision.shSHOWCASE_PASSWORD='ShowcaseDemo1!' ./scripts/seed.shcd capture && npm ci && npx playwright install chromium && cd ..SHOWCASE_PASSWORD='ShowcaseDemo1!' ./scripts/capture.sh iamSHOWCASE_PASSWORD is the password of the seeded demo users on your local stack; pick any.
The console is then on http://localhost, and the screenshots land in
capture/out/.
Always capture through scripts/capture.sh rather than calling the capture package directly.
It enforces an order that matters: authenticate, freeze server-side timestamps, then capture
with the token already in hand. A login performed inside the capture writes last_login,
which the IAM users screen renders, so the file would differ on every run. With the login
inside, 6 of 8 files were byte-identical across two runs; with this order, 8 of 8 are.
| Workflow | What it does | Trigger |
|---|---|---|
| Console Screenshots | Captures the console areas you choose (iam, fleet-ops, admin, general, developers, ledger, extensions, storefront) and uploads them as an artifact. |
Fleetbase release (repository_dispatch: fleetbase-release) or by hand |
| Storefront app screenshots | Captures the Storefront app on iOS and Android, per edition, in dark and light, and uploads a review gallery. | Storefront app release (repository_dispatch: storefront-app-release) or by hand |
Each run also uploads a review gallery (console-gallery, storefront-app-gallery), and
Publish gallery puts the newest of each from main on
GitHub Pages.
Both open a pull request on fleetbase.io only when asked to: the open_pr input, or
OPEN_PR_ON_RELEASE=true for release runs. A plain run only produces images to look at.
Secrets: SHOWCASE_PASSWORD (required), FLEETBASE_IO_TOKEN (only for open_pr), and the
optional map and payment keys described below.
Prefer the workflows over a local run for anything you intend to publish: the runners are amd64, and their fonts render the way the published screenshots do.
1. Everything is on port 80, and that is not negotiable. The published console image is
built ENVIRONMENT=production, so API_HOST is empty and disableRuntimeConfig is true: it
never fetches /fleetbase.config.json, even though the image ships one, so mounting a config
over it does nothing. @fleetbase/ember-core's fetch service then falls back to
`${window.location.protocol}//${window.location.hostname}`, dropping the port. The
API therefore has to answer on the same origin on the default port, which is what
docker/caddy/Caddyfile is for. It is also how a real deployment is shaped, so the screenshots
are more faithful this way, not less.
2. Some prefixes are both a console route and an API route. /storefront/int/v1/dashboard
is the API; /storefront/products is the console. The same goes for ledger, valhalla,
vroom, customer-portal, ai and oauth. The proxy's @api matcher is ordered
accordingly.
Get this wrong and nothing looks broken: an unclaimed API path falls through to the console,
which answers 200 text/html for anything, the app fails to parse JSON, and the screen renders
as an empty table that screenshots perfectly. Two guards exist because of this:
scripts/assert-routes.sh diffs the matcher against php artisan route:list at provision
time, and the capture fails any entry whose /int/ request comes back as HTML.
3. The theme comes from the server, not the browser. Playwright's colorScheme does
nothing on its own: GET int/v1/settings/branding returns brand.default_theme ("dark" on a
stock install) and the console applies it over prefers-color-scheme. The one lever that wins
is the persisted user option at <user uuid>:theme in the @fleetbase/storage:user-options
bucket, which is why the capture fetches the uuid from auth/session before the browser
starts. Discovering it after boot is a race that silently leaves every capture on the brand
theme.
4. An empty table still renders a row. It has the class next-table-empty-state-row, so a
plain tbody tr selector matches it, and an expectRows check of min: 1 would pass on a
table with no data. The manifests' TABLE_ROW excludes it. That is the difference between
"the seeder failed" and "we published a picture of an empty state".
| Path | What |
|---|---|
docker-compose.yml |
The stack. Every image is pinned to ${RELEASE_TAG}; nothing is built. |
docker/caddy/Caddyfile |
The single-origin proxy and its ordered path map. |
scripts/ |
up → provision → seed → capture, plus assert-routes and freeze-timestamps. |
scripts/mobile/ |
The Storefront app capture: tunnel, fixtures, app .env, build and Maestro run. |
seeders/ |
The demo data, bind-mounted into the API container as Database\Seeders\Showcase\. |
capture/ |
The Playwright capture package, its manifests (one per console area), the compare and the galleries. |
COVERAGE.md |
Which fleetbase.io screenshots are generated here, and which are not yet. |
NAMING.md |
How every published file is named. |
They are bind-mounted to /fleetbase/api/database/seeders/Showcase, which is already inside
the image's Database\Seeders\ PSR-4 prefix, so there is no autoload surgery and editing one
is a file save rather than a rebuild. scripts/seed.sh re-dumps the autoloader anyway, which
is what makes adding a class work.
Areas share tables (users, most notably), so each tags its own rows
(seeders/*/Concerns/BelongsTo*.php); otherwise one area's purge would delete another's.
seeders/Concerns/SeedsShowcaseData.php is a deliberate fork of Fleet-Ops' SeedsTestingData.
It has to be: that trait declares SEED_NAME as a constant, and PHP rejects a class that
redeclares a trait constant with a different value, so a showcase seeder using it would tag
its rows fleetops-testing and be purged by Fleet-Ops' own seeder.
There is no Faker: the published API image is installed --no-dev, and fixed values are what
determinism wants anyway. The people and companies in the seed data are fictional; the
places are real Singapore locations.
.github/workflows/storefront-app-screenshots.yml captures the Storefront mobile app on the iOS Simulator (iPhone 16 Pro Max, 1320×2868) and the Android Emulator (Pixel 7 Pro), against the same seeded release stack, and proposes the changed images under public/images/screenshots/storefront-app/{ios,android}/ on fleetbase.io.
- The flows live in the app, in
storefront-app/.maestro/screenshots, so a screen and the flow that walks through it change in the same pull request. This repo provides the stack, the build settings and the publishing. - Trigger: storefront-app's release workflow sends
repository_dispatchstorefront-app-releasewith the tag; or run it by hand withapp_ref,release_tag,platforms,themes(e.g.lightfor a light-only run) andopen_pr. Release runs open the pull request whenOPEN_PR_ON_RELEASEistrue, as for the console. - How the devices reach the stack: iOS only runs on macOS, macOS runners can't run the Docker stack, and iOS only allows HTTPS to unknown hosts. So the stack job opens a
cloudflaredquick tunnel (scripts/mobile/tunnel.sh) and starts the stack with it asAPP_URL, so image URLs resolve too. The captures run in parallel jobs, one per platform and edition: Android on an emulator (Ubuntu, KVM), iOS on the simulator (macOS). They hand over through artifacts (mobile-stack, encrypted withSHOWCASE_PASSWORDand the run id, anddone-<platform>-<edition>), because job outputs only arrive once a job has finished, and the gallery and publish jobs merge the editions' outputs (scripts/mobile/merge-captures.sh). - What the app is built with:
scripts/mobile/fixtures.shreads the seeded store and network keys, the SDK key, the store and food-truck coordinates and the review customer from the database (scripts/mobile-fixtures.php), andscripts/mobile/app-env.shwrites the app's.envper edition: store, network, food trucks and a Network's food trucks (HOME_SCREEN=foodTruckswith the store or network key). For the trucks homes the customer is placed inside the truck's zone. Besides the store's truck, the Storefront seeder gives the network member store Rochor Noodle House a truck (van GBB-1010), so the Network edition has trucks too. Each edition is its own Release build, because the values are compiled in, which is why each edition gets a job of its own. iOS builds arm64 only, and CocoaPods resolves through its CDN instead of cloning the Specs repository the app's Podfile names. - Signing in: the real flow, with Storefront's review-account bypass. Each run generates a code, starts the stack with
STOREFRONT_BYPASS_VERIFICATION_CODEandSTOREFRONT_REVIEW_ACCOUNTS=+6591000201(the seeded customer Ava Chen, who has order history and an order in progress at the store and in the Network, so both editions capture account, orders and checkout), and passes both to the flow. - Determinism: fixed devices and locale, status bar overrides on both platforms (9:41, full battery), animations off on Android, seeded timestamps. Map tiles still vary slightly; the pixel gate absorbs that. There is no DOM on a phone, so the compare is pixels-only.
- Review: every run uploads
storefront-app-gallery, and runs on main publish it to GitHub Pages (.github/workflows/publish-gallery.yml). It shows every expected screen side by side (iOS and Android, dark and light), with gaps marked and failed flows listed (capture/bin/gallery-mobile.mjs, screen list incapture/src/storefront-app-screens.mjs). A run goes red when any flow stopped early. - Output:
capture/out/storefront-app/<platform>/*.png, encoded to lossless WebP bycapture/bin/encode-mobile.mjs(the existing app screenshots are lossless). Dark is unsuffixed, light is-light, names follow NAMING.md.
Optional secrets: STOREFRONT_APP_GOOGLE_MAPS_API_KEY (Android map tiles: a key with the Maps SDK for Android enabled for package com.storefront.app and the React Native debug signing SHA-1 5E:8F:16:06:2E:A3:CD:2C:4A:0D:54:78:76:BA:A6:F3:8C:AB:F6:25; falls back to GOOGLE_MAPS_API_KEY), and the org's STRIPE_TEST_KEY / STRIPE_TEST_SECRET (secrets or variables): the seeded Stripe gateways get the test keys and the app is built with the publishable one, so checkout creates a real test payment.
Locally, with a booted simulator and maestro installed:
URL="$(./scripts/mobile/tunnel.sh)"
APP_URL="$URL" STOREFRONT_BYPASS_VERIFICATION_CODE=123456 STOREFRONT_REVIEW_ACCOUNTS=+6591000201 ./scripts/up.sh
./scripts/provision.sh && SHOWCASE_PASSWORD='…' ./scripts/seed.sh && ./scripts/mobile/fixtures.sh
SIMULATOR_ID=<udid> STOREFRONT_BYPASS_VERIFICATION_CODE=123456 ./scripts/mobile/capture.sh ios ../storefront-app "$URL" storeThe console can render its live operations map with Google Maps instead of Leaflet
(Fleet-Ops → Settings → Map). With a GOOGLE_MAPS_API_KEY in the environment — the
GOOGLE_MAPS_API_KEY repository secret in CI, or GOOGLE_MAPS_API_KEY=… ./scripts/up.sh
locally — scripts/capture.sh runs a second pass after the Leaflet one: it switches the
showcase organization's map provider to Google, captures every manifest entry that declares
map.google with a -google suffix (fleet-ops-live-orders-panel-google.webp,
…-google-light.webp), and switches back.
What to know before enabling it:
- Only the live map (orders index, order details, the drawer's position listing) switches providers. Geofence and service-area editors, place pickers and the tracking page are Leaflet-only in the engine, so there is nothing to capture for them.
- Restrict the key by HTTP referrer to
http://proxy/*(the capture container) andhttp://localhost/*(a laptop). The key is never written into the image set; the console fetches it from the API at runtime. - Google's terms forbid caching tiles, so this pass hits the network live and its output is
not byte-stable between runs.
compare.mjstherefore reports Google captures but does not stage them into the fleetbase.io pull request unless run with--include-google. - Keep Google's logo and attribution visible in anything published.
Output from a native macOS run is visually correct but not byte-comparable to CI — CoreText
and FreeType rasterise glyphs differently, so every file would read as changed. For a
comparable run, execute the capture inside mcr.microsoft.com/playwright:v1.62.1-noble.
The console image is published amd64-only (the API is multi-arch), so it runs under
emulation on Apple silicon — slowly enough that the browser can exceed Playwright's default
30-second navigation timeout. CAPTURE_NAV_TIMEOUT_MS raises it; the default here is already
120s for that reason. If a local capture is painful, run the workflow instead.
The capture package needs Node 22 (engines says so). Node 18 cannot expand the glob in the
test script and cannot load the bundled sharp binary, and the failure looks like missing test
files rather than a wrong runtime.
If your own Fleetbase dev stack already owns port 38000, set WSS_PORT to something else. You
lose the realtime screens — the console has 38000 compiled in — but everything else captures.
Issues and pull requests are welcome, particularly for seed data that makes a screen look more like real use, and for determinism fixes. This repository serves the Fleetbase website first, so changes that would alter published screenshots are reviewed with that in mind. For Fleetbase itself, head to the main repository and its Discord.
Please report security issues privately; see SECURITY.md.
The code is licensed under the GNU Affero General Public License v3.0, the same license as Fleetbase.
Bundled media keeps its own terms: the store and product photos in seeders/Storefront/images/
are from Unsplash under the Unsplash License
(credits in the folder's READMEs); the extension tiles in seeders/Extensions/icons/ use
Font Awesome Free icons (CC BY 4.0). The Fleetbase name
and logo are not covered by the license.