Repository navigation
docs: screenshots of the running SPA in the README - #550
Conversation
Closes #549. Three images, captured from the real built SPA over the seed --profile simulation fixture rather than by hand: - daily entry on a recorded day, by grade - sales orders across their lifecycle, with provenance - reports: production, hen-day %, by-grade totals and the money beside them The capture is a third Playwright config in tools/simulation/ui, run with npm run screenshots. It is deliberately separate from the smoke suite: the run writes files into the repo, so folding it into npm test would rewrite committed images on every unrelated green run. No CI job and no byte-diff gate. Rendering is not deterministic across fonts and antialiasing, so a gate in the shape of the schema-docs check would flake, and a flaky gate gets disabled. The images are refreshed deliberately; the spec header says so. Two fixture-shaped decisions, both recorded in the spec: - The Dashboard is NOT among them. Its Today panel is empty in every fixture capture, because the simulation seeds history up to a few days back, so the hero shot would read as an app with no data. - Daily entry captures YESTERDAY and Reports sets an explicit window ending three days back. The most recent seeded days are drafts, which a report does not count, so the default views are trailing zeroes. Scale 1, measured rather than assumed: 382 KB for the three, against 910 KB at scale 2, and GitHub renders a README image into roughly 890 CSS pixels.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 64986d8805
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await expect(page.getByRole("heading", { name: tEn("sales:ordersHeading") })).toBeVisible(); | ||
| await expect(page.getByText(tEn("sales:loading"))).toHaveCount(0); |
There was a problem hiding this comment.
Assert that sales rows loaded before capturing
If the initial /sales request fails, usePagedList converts rows to an empty array, clears reloading, and renders an error plus the empty-state message; both assertions here still pass because the Orders heading remains visible and the Loading text disappears. The suite then overwrites the committed image with an error/empty screen while reporting success, so assert that the orders table contains the expected populated lifecycle rows before calling capture.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed and fixed in 25cfb5b — and verified by mutation rather than asserted.
The capture now checks that the empty state is absent and that the table holds rows in both lifecycle states:
await expect(page.getByText(tEn("sales:noOrdersMatch"))).toHaveCount(0);
const orders = page.getByRole("table").first();
await expect(orders.getByRole("row").filter({ hasText: tEn("enums:status.Confirmed") })).not.toHaveCount(0);
await expect(orders.getByRole("row").filter({ hasText: tEn("enums:status.Draft") })).not.toHaveCount(0);Mutation check, with the endpoint aborted (page.route("**/api/v1/sales**", r => r.abort())):
Error: expect(locator).toHaveCount(expected) failed
Locator: getByText('No orders match.')
Expected: 0
Received: 1
The old assertions passed under that same mutation, exactly as you described. Restored, and all three captures are green again.
One thing your finding surfaced indirectly: the empty state on this page is sales:noOrdersMatch, not dashboard:noOrdersMessage — two namespaces, two different strings. src/i18n.ts throws on an absent key rather than falling back to English, which is what caught the wrong one on the first run.
| export default defineConfig({ | ||
| testDir: "./specs-screenshots", |
There was a problem hiding this comment.
Include the screenshot suite in TypeScript checks
The new config points at specs-screenshots, but this project’s tsconfig.json includes only specs/**/*.ts, specs-canary/**/*.ts, and the two existing config files. Consequently, npm run typecheck does not examine either this config or the new screenshot spec, so TypeScript-only regressions in the documented capture command can land while the advertised validation remains green; add both paths to the TypeScript project.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed and fixed in 25cfb5b. specs-screenshots/**/*.ts and playwright.screenshots.config.ts are both in the project now.
You were right that the advertised validation was green while proving nothing about the new files — and the fix immediately proved it, twice:
- Coverage check. A deliberate
const __mutant: number = "not a number"in the spec now fails the typecheck (TS2322at that line); removing it restores green. Before the tsconfig change the same mutant compiled clean. - A real defect the vacuous check had hidden. The same run reported
TS6133: 'prefixOf' is declared but its value is never read— a leftover import from an assertion I refuted while writing the spec. It would have shipped.
"npm run typecheck clean" in the PR body was therefore a claim about files the compiler never opened. Corrected.
…ure spec Codex review round 1 on PR 550, two P2 findings, both confirmed: - The sales capture asserted only the Orders heading and the absence of "Loading…". When the initial request fails, usePagedList empties the rows and clears its loading flag, so the page settles into an error plus the empty state with the heading still present — both assertions pass and the run overwrites the committed image with a picture of a failure while reporting success. It now asserts the empty state is absent and that the table holds orders in both lifecycle states. - tsconfig.json listed specs and specs-canary but not specs-screenshots, so "npm run typecheck is clean" said nothing about either new file. Both paths added. Verified rather than asserted: - Typecheck coverage: a deliberate type error in the new spec now fails the typecheck, and removing it restores green. That same run surfaced a real defect the vacuous check had hidden — an unused prefixOf import, left over from a refuted assertion. Removed. - The sales assertion: with the sales endpoint aborted via page.route, the run fails on the empty-state assertion by name. The previous assertions passed under the same mutation. Fixing it also caught a wrong namespace: the empty state is sales:noOrdersMatch, not dashboard:noOrdersMessage. src/i18n.ts throws on an absent key instead of falling back, which is what surfaced it. Images refreshed against the reseeded fixture so all three come from one run.
|
@codex round 1 addressed in 25cfb5b — both findings confirmed real.
Fixing the first one also caught a wrong namespace: the page's empty state is Images regenerated against a reseeded fixture so all three come from one run. Please re-review. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 25cfb5b214
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| .not.toHaveCount(0); | ||
| await expect(orders.getByRole("row").filter({ hasText: tEn("enums:status.Draft") })) | ||
| .not.toHaveCount(0); | ||
|
|
There was a problem hiding this comment.
Wait for sales setup data before capturing
Fresh evidence beyond the earlier order-loading fix is that SalesPage loads the order list independently from its customer/product/grade setup request. When /sales returns first, these row assertions pass while customer cells still show ID prefixes and the New order control is absent; if setup subsequently fails, the page can even switch to its setup-error screen before capture while the test remains green. Wait for a setup-derived signal such as the localized New order button before writing the image.
AGENTS.md reference: AGENTS.md:L188-L191
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed and fixed in 151d4ed. Verified in SalesPage.tsx: customerName is customers.find(...)?.name ?? id.slice(0, 8) (line 175), and setupError replaces the entire screen (line 572) — so both halves of what you describe are real.
The capture now waits for the New order button, which renders only past the setup gate.
Mutation-checked in both directions, and they do not both land on the new line — worth stating rather than claiming the one assertion covers everything:
- Setup delayed past the expect timeout (
page.routeon/customerswith a 20s delay): the rows render, every earlier assertion passes, and the run fails ongetByRole('button', { name: 'New order' })— the new guard, by name. This is the mid-load GUID-column case. - Setup failing outright (
r.abort()): the page becomes thesetupErrorscreen, and the run fails on the Orders heading — the first assertion, not the new one. Already covered, just not by the line you asked for.
Both restored; all three captures green.
Codex review round 2 on PR 550, P2, confirmed: the orders list and the page's setup data (customers, products, grades) are two independent loads, and the rows render either way. customerName falls back to id.slice(0, 8), so a capture taken between them shows a column of GUID fragments where customer names belong; and a setup failure swaps in the setupError screen after the row assertions have already passed. The capture now waits for the New order button, which renders only past the setup gate. Mutation-checked in both directions, because the finding has two halves: - setup DELAYED past the expect timeout: rows render, the earlier assertions pass, and the run fails on the New order button by name. That is the guard. - setup FAILING outright: the page becomes setupError, and the run fails on the Orders heading — the first assertion, not the new one. Worth stating plainly rather than claiming the new line catches both.
|
@codex round 2 addressed in 151d4ed — confirmed real, and specific. Wait for sales setup data before capturing (P2). Verified in Mutation-checked in both directions: with Running total on this PR: 3 confirmed defects across 2 rounds, 0 rejected. Please re-review. |
|
Codex Review: Didn't find any major issues. Keep it up! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
🤖 I have created a release *beep* *boop* --- ## [0.1.0](v0.0.4...v0.1.0) (2026-09-12) ### ⚠ BREAKING CHANGES * log in by farm code, with per-account email identity ([#532](#532)) (#564) ### Features * **accounts:** add Account.Slug (farm code), suspend/reactivate, list-accounts verb ([#531](#531)) ([3fe9754](3fe9754)) * **accounts:** provision additional farms ([#581](#581)) ([006f298](006f298)) * add Aspire local development AppHost ([#567](#567)) ([2c9e6b9](2c9e6b9)) * add configurable worker sale allocation ([#619](#619)) ([0955095](0955095)) * add searchable entity pickers ([#642](#642)) ([60d2053](60d2053)) * **api:** provision-account takes an optional --timezone at creation ([#603](#603)) ([#694](#694)) ([a0aee39](a0aee39)) * **audit:** show the sales-line audit payload as a readable Details column ([#745](#745)) ([#749](#749)) ([d26d389](d26d389)) * **auth:** add ApplicationUser.StepUpLogoutEpoch column ([#338](#338)) ([#554](#554)) ([18306ee](18306ee)) * certify over-cap simulation fixture bands ([#633](#633)) ([a67b2e1](a67b2e1)), closes [#627](#627) * **cli:** rename-account verb to change a farm code ([#732](#732)) ([#733](#733)) ([4b70559](4b70559)) * **customers:** edit existing customer details ([#625](#625)) ([#626](#626)) ([062a55c](062a55c)) * **jobs:** single-runner leader gate for the durable job worker ([#271](#271)) ([#555](#555)) ([4148f9b](4148f9b)) * let owners change user email addresses ([#605](#605)) ([842347b](842347b)) * log in by farm code, with per-account email identity ([#532](#532)) ([#564](#564)) ([68adb62](68adb62)) * **ratelimit:** distributed IP-keyed auth limiters ([#544](#544)) ([#558](#558)) ([ec14972](ec14972)) * **ratelimit:** distributed per-account report concurrency cap with local-ceiling fallback ([#545](#545)) ([#559](#559)) ([1522e4e](1522e4e)) * **sales:** mark discounted lines, total the discount, and show it in the Orders list ([#723](#723), [#724](#724)) ([#741](#741)) ([1a07441](1a07441)) * **sales:** record list, old and new price in the order-line audit payload ([#722](#722)) ([#742](#742)) ([97c866f](97c866f)) * **sales:** refuse an over-ceiling confirm from a Sales user ([#727](#727)) ([#766](#766)) ([8c0792a](8c0792a)) * **sales:** show what each order still owes, and filter the list to unpaid ([#771](#771)) ([ca59d68](ca59d68)) * **sales:** snapshot the list price on the order line and show the discount ([#734](#734)) ([cffed5e](cffed5e)) * **sales:** snapshot the product name and unit in the order-line audit payload ([#747](#747)) ([#748](#748)) ([0481c06](0481c06)) * scope Worker reads to assigned flocks ([#388](#388)) ([#611](#611)) ([5884a9a](5884a9a)) * shared-state ports with Redis + in-process fallback ([#543](#543)) ([#552](#552)) ([f767fa9](f767fa9)) * suspend-account / reactivate-account operator verbs ([#534](#534)) ([#573](#573)) ([d0be26c](d0be26c)) * **tenancy:** write-side tenant guard + single-assignment TenantContext ([#546](#546)) ([#561](#561)) ([f371f1d](f371f1d)) * **web:** dashboard rework — capture-status tiles, 14-day trend, stock as a stacked bar ([#654](#654)) ([396ba23](396ba23)) * **web:** date-range filters on audit and expenses, and the stock lot filter gets its bounded toolbar ([#666](#666), [#667](#667), [#653](#653)) ([94b188f](94b188f)) * **web:** elevation hierarchy and sentence-case labels ([#651](#651), [#652](#652)) ([#661](#661)) ([28db4c7](28db4c7)) * **web:** Expenses and Audit keep a clear-filters control while rows are still showing ([#679](#679)) ([#697](#697)) ([b859982](b859982)) * **web:** expenses filters by a date range like its sibling screens ([#667](#667)) ([f13858f](f13858f)) * **web:** key the farm brand palette per farm ([#586](#586)) ([#600](#600)) ([7183a43](7183a43)) * **web:** let operators forget remembered farms ([#598](#598)) ([577d94e](577d94e)) * **web:** one-line provenance, bounded date filters, and empty states that invite action ([#653](#653), [#655](#655)) ([#668](#668)) ([80b53f4](80b53f4)) * **web:** prefill the farm code from ?farm= and remember it ([#535](#535)) ([#588](#588)) ([b7f5cc6](b7f5cc6)) * **web:** split authenticated routes into lazy chunks ([#620](#620)) ([5089271](5089271)) * **web:** the audit log filters by a date range, and says which window is empty ([#666](#666)) ([63027e0](63027e0)) * **web:** typeset numbers as numbers and refresh the Help glossary ([#650](#650), [#657](#657)) ([af4fe11](af4fe11)) ### Bug fixes * **api:** order same-instant audit events by a durable monotonic key ([#700](#700)) ([8fcf084](8fcf084)) * **api:** print the farm code from bootstrap-admin ([#589](#589)) ([#594](#594)) ([34032ac](34032ac)) * **audit:** show the price a line sold for, not its list price ([#759](#759)) ([e6b37d0](e6b37d0)) * **audit:** store catalog enums by name and guard the add-item transaction shape ([#751](#751)) ([23609ff](23609ff)) * **auth:** reject invalid account claims ([#622](#622)) ([8d6c7fe](8d6c7fe)) * **auth:** require step-up for durable user access ([#360](#360)) ([#607](#607)) ([f767dce](f767dce)) * **ci:** bound the npm audit calls and give the web job room to finish ([#686](#686)) ([153b7a8](153b7a8)) * **ci:** escalate the audit bound to SIGKILL, so it actually bounds ([#686](#686)) ([a0c8f4e](a0c8f4e)) * **ci:** fail closed on invalid vulnerability config ([#621](#621)) ([1690db8](1690db8)) * **ci:** lockfix covers the two AppHost lock files, derived from the sln ([efb05e6](efb05e6)) * **ci:** lockfix covers the two AppHost lock files, derived from the sln ([8986d77](8986d77)) * **ci:** remove invalid XML comment from nuget.lockfix.config ([#541](#541)) ([5f1bc0a](5f1bc0a)) * **ci:** the advisory vuln gate no longer blocks on an unusable report ([#686](#686)) ([aaf6934](aaf6934)) * **ci:** the advisory vuln gate no longer blocks on an unusable report ([#686](#686)) ([64f1f53](64f1f53)) * **i18n:** tl help text names the saleable flag and unit-system setting what their labels call them ([#688](#688)) ([#696](#696)) ([bfd24d7](bfd24d7)) * **infra:** AccountId must be a non-nullable Guid or both tenant write layers refuse ([#673](#673)) ([#695](#695)) ([2470c4e](2470c4e)) * require step-up for flock scope changes ([#609](#609)) ([4151f89](4151f89)) * **sales:** keep a line's discount markers agreeing while its price is edited ([#752](#752)) ([#753](#753)) ([c159b4b](c159b4b)) * **sales:** say which kind of missing list price a line has ([#774](#774)) ([489180e](489180e)) * scope legacy logout to selected farm ([#624](#624)) ([fae8d82](fae8d82)) * **seed:** drain the daily-entry lock sweep so deep simulation fixtures validate ([#644](#644)) ([730fa23](730fa23)), closes [#638](#638) * **tenancy:** AccountId is a concurrency token, so the database refuses a detached cross-tenant write ([#562](#562)) ([4d1dfa3](4d1dfa3)) * **tenancy:** AspNetUserRoles carries a tenant column, so a role write naming another farm's user is refused ([#670](#670)) ([fc0552a](fc0552a)) * **tests:** bump the image-pin allow-list counts for the AppHost LocalPorts tests ([#593](#593)) ([58d3056](58d3056)) * **tests:** the OTLP collector survives a lost port race and ignores traffic that is not an export ([#672](#672), [#676](#676)) ([#677](#677)) ([965c737](965c737)) * **web:** a scoped audit view filtered to nothing names both the record and the range ([#666](#666)) ([41bbfe1](41bbfe1)) * **web:** an abandoned dialog attempt's success no longer hijacks the replacement on Customers, Daily Entry, Flocks, Grades and Products ([#703](#703)) ([#705](#705)) ([85605db](85605db)) * **web:** an abandoned dialog attempt's success no longer hijacks the replacement on Inventory, Expenses, History and Stock ([#703](#703)) ([#706](#706)) ([60a4997](60a4997)) * **web:** an abandoned edit's success no longer hijacks the dialog that replaced it on Users ([#703](#703)) ([#710](#710)) ([778faab](778faab)) * **web:** an abandoned order attempt's success no longer hijacks the dialog that replaced it ([#702](#702)) ([522c699](522c699)) * **web:** capture screens open on the flock you last used, and assigning one no longer guesses ([#646](#646)) ([#699](#699)) ([7f8f317](7f8f317)) * **web:** constrain dialog session helpers to declared scopes ([#715](#715)) ([389e3c8](389e3c8)) * **web:** date validation gets one boundary table instead of one case per review round ([#666](#666)) ([215f830](215f830)) * **web:** keep a paged window and an item panel on the user's newest intent ([#645](#645)) ([d81bccf](d81bccf)) * **web:** keep Sales order panels closed after pending writes ([#711](#711)) ([f0f7492](f0f7492)) * **web:** keep Sales panels closed after pending Open reads ([#716](#716)) ([620411f](620411f)) * **web:** make login take the cross-tab cookie lock so a racing refresh cannot restore the wrong session ([#648](#648)) ([ff18beb](ff18beb)) * **web:** make the entity picker read as a search field and focus it on open ([#736](#736)) ([66ef667](66ef667)), closes [#735](#735) * **web:** page truncated customer and movement tables with usePagedList ([7cfe4d6](7cfe4d6)) * **web:** reconcile Sales line edits with refreshed orders ([#717](#717)) ([d7dd2c9](d7dd2c9)) * **web:** the audit date filter accepts low-numbered years, and its empty state covers every narrowing ([#666](#666)) ([af52d25](af52d25)) * **web:** the audit date filter rejects impossible dates, and its history guard actually guards ([#666](#666)) ([8d51846](8d51846)) * **web:** the expense range bounds are not capped at today, which the month-end default exceeds ([#667](#667)) ([7e01864](7e01864)) * **web:** the help text calls the expiry field what the field calls itself ([#666](#666)) ([2fd1f3c](2fd1f3c)) * **web:** the stock lot date range sits in the bounded toolbar ([#653](#653)) ([43dec5e](43dec5e)) ### Refactoring * **web:** extract SalesPage's dialog-write wrapper into a shared useDialogAction hook ([#703](#703)) ([#704](#704)) ([60ee9d9](60ee9d9)) ### Documentation * add k6 preparation steps to the dev-database fixture runbook ([#643](#643)) ([a4f1f09](a4f1f09)) * add runbook for loading the simulation fixture into a dev database ([#639](#639)) ([2d143b8](2d143b8)) * **agents:** a PR closes its issue from the body, not the title ([#744](#744)) ([39be13c](39be13c)) * **agents:** drop the commit and push gate, and require screenshots on UI changes ([#757](#757)) ([6225172](6225172)) * **agents:** find guards by grepping registry readers; amend issues a PR overtakes ([#580](#580)) ([fe3fde8](fe3fde8)) * **agents:** the Playwright specs have been in CI since 2026-08-08 ([#768](#768)) ([68ee612](68ee612)) * **aspire:** record the second local database and pin the AppHost dashboard ports ([#623](#623)) ([713b941](713b941)) * compress AGENTS.md to one paragraph per rule, and draw the two orders that matter ([#551](#551)) ([997ae8a](997ae8a)) * item 7 names each screen's actual initial filter value ([#666](#666)) ([70a53d8](70a53d8)) * multi-farm tenancy decision record and AGENTS/GLOSSARY sync ([#537](#537)) ([#601](#601)) ([2c34771](2c34771)) * name the scoped filtered-empty key and state the [#653](#653) relationship plainly ([#666](#666)) ([0e93dac](0e93dac)) * note that a PackageReference in Directory.Build.props is invisible to the dependency graph ([4845724](4845724)) * **plans:** commit the [#722](#722) and [#745](#745) design records ([#754](#754)) ([c942fcd](c942fcd)) * record [#579](#579) as won't-fix — suspension is immediate for use, not issuance ([#582](#582)) ([7a3be40](7a3be40)) * record the [#508](#508) audit ordering key and the tracked-file guard lesson ([#701](#701)) ([08964e9](08964e9)) * **runbooks:** add procedure to rename the default farm's code after upgrade ([#731](#731)) ([2f6e242](2f6e242)) * screenshots of the running SPA in the README ([#550](#550)) ([711488a](711488a)) * **sim:** commit the dashboard screenshot, capture the palette matrix, and record the [#651](https://github.com/mforce/cluckwork/issues/651)/[#652](https://github.com/mforce/cluckwork/issues/652) conventions ([#660](#660), [#662](#662), [#663](#663), [#664](#664)) ([#665](#665)) ([930ea30](930ea30)) * specify searchable entity picker ([#641](#641)) ([91d4300](91d4300)) * split the README into audience-scoped docs and adopt repo-template scaffolding ([#548](#548)) ([b3f3fcf](b3f3fcf)) * surface Aspire local development workflow ([#568](#568)) ([a343baa](a343baa)) * **web:** record the per-screen idempotency-key policies and runWrite's refresh contract ([#703](#703)) ([#707](#707)) ([8bee651](8bee651)) * **web:** the date-cap help text covers every stocked item, not only feed ([#666](#666), [#667](#667)) ([c8433c5](c8433c5)) * **web:** the help text claims only what is true of recording, and says nothing about filter caps ([#666](#666), [#667](#667)) ([e2f63d1](e2f63d1)) * **web:** the help text describes the date-range filters that shipped ([#666](#666), [#667](#667)) ([c3275b7](c3275b7)) * **web:** the help text stops describing a cap the filters no longer have ([#666](#666), [#667](#667)) ([49654cd](49654cd)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: cluckwork-lockfix[bot] <309265648+cluckwork-lockfix[bot]@users.noreply.github.com>
Closes #549.
What and why
The README describes a shipped product and shows none of it. Three images now do, and they are generated, not hand-captured — the capture is a Playwright spec driving the real built SPA over the same
seed --profile simulationfixture the smoke suite and k6 use.daily-entry.png(hero)sales.pngreports.pngHow it was verified
npm run screenshotsagainst a freshlyreset.sh-seeded stack: 3 passed.npm run typecheckclean. Every frame was reviewed before commit — no auth screen, no token, no.sim-cast.jsonvalue; the visible identifiers are the fixture's synthetic@sim.localpersonas andSim Customer N.Decisions worth reviewing
A third Playwright config, not a spec in
specs/. This run writes files into the repo. Folding it intonpm testwould rewrite committed images on every unrelated smoke run and turn a green run into a dirty working tree.No CI job, no byte-diff gate. Rendering is not deterministic across fonts, antialiasing and GPU paths, so a gate shaped like the #417 schema-docs check would flake — and a flaky gate gets disabled, which is worse than an honest "no gate". The staleness contract is stated in the spec header: refresh when a captured screen changes, and at each release.
The Dashboard is deliberately absent, though #549 listed it. Its Today panel is empty in every fixture capture — the simulation seeds history up to a few days back, so both flocks read no entry and eggs collected today is 0. True of the fixture, misleading as a product's front page, and unfixable without the capture writing today's entries into the shared database. Same reason Daily entry captures yesterday and Reports sets an explicit window ending three days back: the most recent seeded days are drafts, which a report does not count, so both default views are trailing zeroes. Say the word and I will put a Dashboard shot back with its emptiness accepted.
Scale 1, measured not assumed. 382 KB for the three at scale 1 against 910 KB at scale 2; GitHub renders a README image into roughly 890 CSS pixels, so a 1280-wide capture still carries ~1.4x the pixels of its slot. Every regeneration adds its bytes to history permanently, which is what makes the 2.4x worth avoiding.
Four selector traps hit while writing this, each fixed with the reason in a comment — they are the kind that pass wrongly rather than fail loudly:
getByText("Eggs available")is ambiguous with the Stock panel's "{{count}} eggs available."getByLabel("To")on Reports matches the theme toggle's "Switch to night mode".allGradedShortis present but CSS-hidden on the captured day — a wait on it times out claiming the data never arrived, when it had.Checklist
tools/simulation/ui/README.mdgains the third mode)..sim-cast.json.farmToday(farm.timeZoneId), not a UTC date.