Skip to content

docs: screenshots of the running SPA in the README - #550

Merged
mforce merged 3 commits into
mainfrom
docs/readme-screenshots
Aug 16, 2026
Merged

mforce merged 3 commits into
mainfrom
docs/readme-screenshots

Conversation

@mforce

@mforce mforce commented Aug 16, 2026

Copy link
Copy Markdown
Owner

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 simulation fixture the smoke suite and k6 use.

Image Claim it evidences
daily-entry.png (hero) The core loop: counts → grading → sellable
sales.png Orders across their lifecycle, with per-row provenance
reports.png "the numbers that matter" — hen-day %, by-grade, money

How it was verified

npm run screenshots against a freshly reset.sh-seeded stack: 3 passed. npm run typecheck clean. Every frame was reviewed before commit — no auth screen, no token, no .sim-cast.json value; the visible identifiers are the fixture's synthetic @sim.local personas and Sim Customer N.

Decisions worth reviewing

A third Playwright config, not a spec in specs/. This run writes files into the repo. Folding it into npm test would 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".
  • allGradedShort is present but CSS-hidden on the captured day — a wait on it times out claiming the data never arrived, when it had.
  • A field this spec typed into keeps its focus ring and text selection, so the first daily-entry capture showed half a date highlighted. The capture helper blurs first.

Checklist

  • The PR title is a conventional commit, and is the release note I want.
  • Docs updated in this PR (tools/simulation/ui/README.md gains the third mode).
  • No hardcoded credential — personas come from the git-ignored .sim-cast.json.
  • No hardcoded English — every selector resolves through the SPA's own catalogs.
  • Farm clock respected — farmToday(farm.timeZoneId), not a UTC date.
  • No new Action, no dependency change, no migration.

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +157 to +158
await expect(page.getByRole("heading", { name: tEn("sales:ordersHeading") })).toBeVisible();
await expect(page.getByText(tEn("sales:loading"))).toHaveCount(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +21 to +22
export default defineConfig({
testDir: "./specs-screenshots",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. Coverage check. A deliberate const __mutant: number = "not a number" in the spec now fails the typecheck (TS2322 at that line); removing it restores green. Before the tsconfig change the same mutant compiled clean.
  2. 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.
@mforce

mforce commented Aug 16, 2026

Copy link
Copy Markdown
Owner Author

@codex round 1 addressed in 25cfb5b — both findings confirmed real.

  • Assert that sales rows loaded before capturing (P2). Fixed and mutation-checked: with the sales endpoint aborted, the run now fails on getByText('No orders match.') expecting 0, received 1. The previous assertions passed under that same mutation, exactly as described.
  • Include the screenshot suite in TypeScript checks (P2). Fixed. Proven by mutant: a deliberate type error in the new spec now fails npm run typecheck and compiled clean before. It also surfaced a real leftover — an unused prefixOf import — that the vacuous check had hidden, so the PR body's "typecheck clean" was a claim about files the compiler never opened.

Fixing the first one also caught a wrong namespace: the page's empty state is sales:noOrdersMatch, not dashboard:noOrdersMessage.

Images regenerated against a reseeded fixture so all three come from one run. Please re-review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +175 to +178
.not.toHaveCount(0);
await expect(orders.getByRole("row").filter({ hasText: tEn("enums:status.Draft") }))
.not.toHaveCount(0);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.route on /customers with a 20s delay): the rows render, every earlier assertion passes, and the run fails on getByRole('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 the setupError screen, 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.
@mforce

mforce commented Aug 16, 2026

Copy link
Copy Markdown
Owner Author

@codex round 2 addressed in 151d4ed — confirmed real, and specific.

Wait for sales setup data before capturing (P2). Verified in SalesPage.tsx: customerName falls back to id.slice(0, 8) and setupError replaces the whole screen, so a capture between the two loads publishes GUID fragments in the customer column. The capture now waits for the New order button.

Mutation-checked in both directions: with /customers delayed past the expect timeout the run fails on the New order button by name (the mid-load case, which is the new guard); with /customers aborted it fails on the Orders heading instead (the setup-error case, already covered by the first assertion). Said plainly on the thread rather than claiming one line catches both.

Running total on this PR: 3 confirmed defects across 2 rounds, 0 rejected. Please re-review.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 151d4ed158

ℹ️ 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".

@mforce
mforce merged commit 711488a into main Aug 16, 2026
10 checks passed
@mforce
mforce deleted the docs/readme-screenshots branch August 16, 2026 20:28
mforce pushed a commit that referenced this pull request Sep 12, 2026
🤖 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs: generated screenshots of the running SPA in the README

1 participant