Skip to content

feat(web): add a Lay rate range switcher and page the Dashboard panels - #940

Merged
mforce merged 17 commits into
mainfrom
feat/914-915-dashboard-range-paging
Sep 24, 2026
Merged

mforce merged 17 commits into
mainfrom
feat/914-915-dashboard-range-paging

Conversation

@mforce

@mforce mforce commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner

What this changes

#914 — the Lay rate card's window is chosen, not fixed. A Range control
under the flock scope offers the last 7 or 14 finished days, or a
Custom range… of two plain date fields with Apply, capped at 14 days.
The choice is remembered per device and per farm (#535's account-scoped storage),
re-validated on read rather than trusted, and a remembered window this build can
no longer draw falls back to the default.

The card draws one bar per day and nothing coarser (owner, 2026-09-23). 14
days is what the strip holds at a readable width — 22px slots and 2px gaps in
the phone card's 342px of plot (#912). A longer range is refused in the form
rather than redrawn at another scale. The 30-day preset, custom ranges up to
90 days and the expanded view that could carry them moved to #941
, which this
PR does not build. The week bucketing #914 originally specified was built,
reviewed, and then deleted rather than left unreachable behind the cap, so
#941 can choose its own display from a clean slate.

#915 — both busy panels page inside themselves. Morning collection shows
8 houses a page on desktop and 6 on a phone, missing houses still first, so
every house on a 100-house farm is reachable without leaving the Dashboard. Recent
orders shows 5 a page and fetches the next five on demand through the shared paged
list. Neither panel gained an inner scroll region.

Component plan (docs/designs/822-mui-revamp.md)

Decisions taken, and where they differ from the issues

  • Page sizes: 8 desktop, 6 phone. Owner-confirmed 2026-09-23.
  • Presets are 7 and 14 days; custom ranges are capped at 14. Both are what
    the strip can draw one bar a day. Longer ranges are web: expand the Lay rate chart into a centred overview + scrollable daily view for ranges beyond 14 days #941's.
  • The strip draws the chosen window only. The equal-length window before it is
    still fetched — in the same single request, split client-side by
    splitProductionReport — but only feeds the hen-day comparison.
  • The week hairline now marks each seven-day boundary inside a daily strip. On
    the fourteen-day default that is index 7, exactly where it has always been, so
    the shipped render is unchanged. A seven-day window has no internal boundary and
    draws none; a weekly strip needs none, because every bar is already a week.
  • A custom range ends on the farm's yesterday at the latest, because the card
    counts finished days only (owner decision A, web: redesign Dashboard around the Operations Desk direction #906). Longer than 14 days is
    refused in the form, never silently shortened, and a range leaving no room for
    its own comparison window is refused too.
  • The pager is previous/next with a range label, not numbered pages. The
    /sales list returns a bare array with no count, so the number of order pages is
    unknown until a short page arrives; numbered pages there would either invent a
    total or grow under the reader's finger. The label says Orders 1 to 5 until the
    last page lands and Orders 11 to 12 of 12 after it. Houses, whose total IS
    known, read Houses 1 to 6 of 100 throughout. One control serves both panels,
    and at 390 a numbered pager cannot hold 44px targets inside the card's 342px.
  • trendPanelTitle is now "Lay rate trend". It was "Last 14 days", which the
    range control makes false; it survives as the card's accessible name, and the
    head's duplicate caption is gone because the control states the window.

Tests

Measured on this branch, not recalled.

file before after
web/src/lib/dashboard.test.ts 35 42
web/src/lib/dates.test.ts 43 47
web/src/lib/layRateRange.test.ts — 13
web/src/routes/Dashboard.test.tsx 84 97
web/src/components/DayStrip.test.tsx 15 15
whole web suite 3403 (140 files) 3440 (141 files)

npm run typecheck clean; npm run test:coverage green. The full quick Playwright
suite at this head, both projects, against an isolated cw914a stack: 97 passed,
1 intentionally skipped, 0 failed
. Two of those 97 are new (phone.spec.ts), so
the same suite was 95 before.

Mutation checks

Every new guard was shown failing on a mutation of the code it covers, then green
again. Fourteen mutations, fourteen reds.

# mutation went red
1 trendWindow preset starts a day late layRateRange.test.ts (2 cases)
2 customRangeError accepts 91 days layRateRange.test.ts (2 cases)
3 parseStoredRange trusts storage without re-validating layRateRange.test.ts
4 inclusiveDays drops its second end dates.test.ts (3 cases)
5 a week bar plots its total instead of its daily rate dashboard.test.ts (2 cases)
6 bucketing starts one day late dashboard.test.ts, Dashboard.test.tsx
7 panelPage trusts a stale page number dashboard.test.ts
8 no week boundary in a daily strip dashboard.test.ts
9 desktop pages six houses instead of eight Dashboard.test.tsx
10 the orders label claims a total it does not have Dashboard.test.tsx (2 cases)
11 the chosen range is not written to storage Dashboard.test.tsx
12 the strip plots the comparison window too Dashboard.test.tsx (3 cases)
13 the weekly readout's reserved row goes back to 2.2rem phone.spec.ts (real browser)
14 the page content loses its bottom padding under the tab bar phone.spec.ts (real browser)

Mutations 13 and 14 were injected into the running app through a stylesheet
override, against the same isolated stack, so both browser guards were observed red
and then green without rebuilding.

Defect found and fixed inside this slice

The first capture round showed a real defect the unit tests could not see: a weekly
readout carries two dates, a day count and two figures, which overflowed the
2.2rem row .tipdock reserves and grew upward over the scale caption. Fixed
by shortening the four week strings and giving a bucketed dock the full card width
and a 3.6rem reserved row (ba4f40b). Measured afterwards in the real browser at
1280 and 390 in all three locales — worst case 14.8px of headroom above the row
and 8px below, tl included — and pinned by the new phone.spec.ts guard.

Deletions (grepped across the whole repo, tools/simulation/ui included)

git grep -n "<name>" -- ':!graphify-out', after the change:

symbol remaining references
TILE_CAP 0
visibleTiles 0
moreFlocks (i18n key, 3 locales) 0
recentCount (dayStrip parameter) 0

The only hits anywhere are stale entries inside graphify-out/graph.json, which is
regenerated output.

Documentation

specs/product/GLOSSARY.md, the SPA Help page and the in-app glossary are updated
in en, es and tl, each locale using its own control labels (#688): Range /
Período / Saklaw, Custom range… / Período personalizado… / Sariling saklaw…, From /
Desde / Mula, To / Hasta / Hanggang, Apply / Aplicar / Ilapat. "Capture status" and
the renamed "Lay rate strip scale" entries were rewritten, and "Lay rate range" is a
new entry; helpGlossary.test.ts's own guard requires every in-app term to exist in
the spec glossary, so the rename moved in lock step.

Known limitation, tracked separately

At the drain ceiling the Morning collection progress bar still renders a
determinate percentage from an incomplete entry list, so it can read lower than
the farm's real position while the notice below it explains why. The owner is
keeping the ceiling as it stands; the bar and a better design for the whole
incomplete-data state are tracked in #942 (low priority).

Review round 1 (Codex gpt-6-sol, of ff5d5ee)

Five findings, all fixed, each with a test that failed first; finding 4 is
settled by the deletion of week bucketing. The owner also asked for the
Dashboard's "All flocks" scope button, which this PR's own captures showed as
brand-on-dark: it was 1.18:1 and is now 9.35:1, fixed in the theme for every
text and outlined primary Button (fourteen call sites across seven screens),
not with a one-off sx. Detail in the round-1 reply comment.

Closes #914
Closes #915

@mforce

mforce commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner Author

Dashboard, before and after — default-farm (100 houses), 1:1

Both stacks built from tools/simulation/docker-compose.sim.yml under isolated
compose projects on port 8114: before cw914b at 0c0faad (this branch's
base), after cw914a at ff5d5ee (this head). Same farm, same fixture, same
viewports.

The head of the page: the Morning collection list, which the after frames page at
8 a screen on desktop and 6 on a phone.

Before, Dashboard top, 1280 light

After, Dashboard top, 1280 light

Before, Dashboard top, 1280 dark

After, Dashboard top, 1280 dark

Before, Dashboard top, 390 light

After, Dashboard top, 390 light

Before, Dashboard top, 390 dark

After, Dashboard top, 390 dark

After frames captured at 0b64395.

@mforce

mforce commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner Author

Recent orders and the Lay rate card, before and after

Scrolled to the lower half of the same page. What changed here: the Morning
collection foot loses its "88 more flocks" link and gains a pager; Recent orders
gains one; the Lay rate card gains the Range control and loses the head's fixed
"Last 14 days" caption, which the control now states. The strip itself is
unchanged at the 14-day default — fourteen daily bars, the week hairline at index
7, the average line, the same legend.

Before, orders and Lay rate, 1280 light

After, orders and Lay rate, 1280 light

Before, orders and Lay rate, 1280 dark

After, orders and Lay rate, 1280 dark

Before, Recent orders, 390 light

After, Recent orders, 390 light

Before, Recent orders, 390 dark

After, Recent orders, 390 dark

Before, Lay rate card, 390 light

After, Lay rate card, 390 light

Before, Lay rate card, 390 dark

After, Lay rate card, 390 dark

After frames captured at 0b64395.

@mforce

mforce commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner Author

#914 — the range switcher (after only; there is no before for a control that did not exist)

Scoped by the owner on 2026-09-23: this card draws one bar per day and
nothing coarser
, so it offers only what it can draw. The 30-day preset and
the custom range up to 90 days, with the expanded view that could carry them,
moved to #941 and are not in this PR.

  • Last 14 days — the default, a full fortnight of daily bars with the week
    hairline at day 7.
  • Last 7 days — seven wider bars, one day's readout open, and no hairline:
    a seven-day window has no internal boundary.
  • Custom range, 10 days — the two plain date fields and Apply.
  • A 20-day custom range is refused in the form — "Choose a range of at most
    14 days." The card keeps plotting the window the reader last accepted rather
    than redrawing it at a scale nobody asked for.

The "All flocks" scope button above the control is also the contrast fix this
round carries: it was raw --brand on the dark card at 1.18:1, and is now the
same --stat-accent the tabs use, measured at 9.35:1.

1280 light 1280 dark
Last 14 days, 1280 light Last 14 days, 1280 dark
Last 7 days, 1280 light Last 7 days, 1280 dark
Custom 10-day range, 1280 light Custom 10-day range, 1280 dark
A 20-day range refused, 1280 light A 20-day range refused, 1280 dark
390 light 390 dark
Last 14 days, 390 light Last 14 days, 390 dark
Last 7 days, 390 light Last 7 days, 390 dark
Custom 10-day range, 390 light Custom 10-day range, 390 dark
A 20-day range refused, 390 light A 20-day range refused, 390 dark

After frames captured at 0b64395.

@mforce

mforce commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner Author

#915 — paging inside the panels (after only)

  • Morning collection, page 1 — "Houses 1 to 8 of 100" at 1280, "Houses 1 to 6
    of 100" at 390, missing houses still first, previous disabled.
  • Morning collection, page 3 — "Houses 17 to 24 of 100", both steps live. The
    progress bar and the "0 of 100 houses in" caption above still answer for the
    whole farm, never for the page on screen.
  • Recent orders, page 2 — "Orders 6 to 10", fetched from the server when the
    step was taken. It gains "of N" only when a short page proves what N is.
  • Maximum scroll at 390 — the pagers against the fixed tab bar. Nothing in the
    page sits under it; phone.spec.ts now asserts this and was checked red by
    removing the content's bottom padding.

Morning collection page 1, 1280 light

Morning collection page 1, 1280 dark

Morning collection page 1, 390 light

Morning collection page 1, 390 dark

Morning collection page 3, 1280 light

Morning collection page 3, 1280 dark

Morning collection page 3, 390 light

Morning collection page 3, 390 dark

Recent orders page 2, 1280 light

Recent orders page 2, 1280 dark

Recent orders page 2, 390 light

Recent orders page 2, 390 dark

Maximum scroll clear of the tab bar, 390 light

Maximum scroll clear of the tab bar, 390 dark

After frames captured at 0b64395.

Presets 7 and 14, custom ranges capped at 14 days. Week bucketing and its
strings are deleted rather than left unreachable; longer ranges move to #941.
Also fixes the brand-on-dark contrast of every text and outlined primary
Button through the theme.
…ccent

MUI v6 replaced Button's textPrimary/outlinedPrimary slots with variants, so
the first override was dropped in silence. A render assertion now reads the
colour the DOM actually gets, which is what caught it.
@mforce

mforce commented Sep 23, 2026

Copy link
Copy Markdown
Owner Author

Codex (gpt-6-sol) review round 1 of ff5d5ee — five findings, all fixed

Every product finding has a test that was observed failing first. Head is now
473e093f29c8887eebb50a7919a5b1900559ec18.

1 (P1) — the Recent orders fetch retried without anyone asking

Confirmed, and the root cause was the shape rather than the condition: an effect
reconciling a page NUMBER against the loaded rows. A refused page left hasMore
true, so the effect re-issued it every time loading settled, and the panel
showed a page error over rows that had loaded perfectly well.

Asking is now something the reader does, once. showNextOrders owns its request,
the page number moves only after rows arrive, and a refusal is offered back as an
inline retry under the rows rather than replacing them. The panel error is now
reserved for a read that left nothing on screen (orderRows.length === 0).

Fix 11d5d81. Failing-first test: "asks once when a next page fails, keeps the
loaded page, and retries only when asked"
— page 2 fails, exactly one request at
offset=5 and still one after a wait, page-1 rows still present, then the retry
succeeds and the count reaches two.

2 (P2) — the pager moved before the rows existed and never clamped

Confirmed for all three cases. usePagedList.loadMore now resolves with what
became of the request instead of nothing: {status:"loaded", rows},
{status:"refused"} or {status:"dropped"}. The count cannot come from rows —
after the await React has scheduled the update but not necessarily rendered it —
and rows: 0 is a real answer, which is how the last page announces itself.

Exactly 5 orders now settles at "Orders 1 to 5 of 5" with Next disabled; exactly
10 settles at "Orders 6 to 10 of 10"; Next is disabled while a request is in
flight. Fix 11d5d81. Three failing-first tests, one per case.

One thing worth reporting: my first attempt guarded the double tap with a ref and
claimed it was load-bearing. A mutation check proved it was not — fireEvent
flushes React state, so the second tap never reached the handler, and the hook's
own loadingRef already refused the duplicate request. What it did mask was a
real defect of mine: that refusal came back as the same null a failure did, so a
reader who tapped twice would have been shown an error. Hence the tri-state above,
pinned by four new tests in usePagedList.test.tsx where the interleaving is
actually reachable.

3 (P2) — Morning collection stopped at the first 500 flocks

Confirmed. I chose to drain every page, not to restore a remainder link.
Reason: with a cap we never learn the farm's size, so the progress bar and the
"N of M houses in" caption could not be based on the complete set, which is what
the finding asks for — and a link would reintroduce exactly what #915 removed.

The daily-entry list was capped the same way, so draining flocks alone would have
marked houses 501+ as "not recorded"; both lists drain through one helper. Below
500 houses this costs no extra request at all. The loop has a ceiling of 20 pages
purely so it terminates; at it the counts say "at least" rather than a figure they
cannot stand behind. Fix 11d5d81. Failing-first test: a 1,201-flock farm reaches
"0 of 1201 houses in", "Houses 1 to 8 of 1,201", and offsets [0, 500, 1000].

4 (P2) — the 1-day last bucket took the daily wording

Confirmed, and settled by deletion. The owner scoped this card to ranges it
can draw as daily bars (presets 7 and 14, custom capped at 14), so week bucketing
is no longer reachable. Rather than leave it behind the cap, it is gone: the
bucket model, weekPeriods/stripPeriods, the bucketed flag, the twelve weekly
i18n keys in three locales, the taller readout row and its Playwright guard.
Verified zero remaining references across the whole repo including
tools/simulation/ui. Long ranges and whatever display carries them are #941.

I had fixed the finding first (reading the wording from the strip's mode rather
than the bucket's length, with its own failing-first test); that commit is in the
history at 11d5d81 and the deletion follows in ad8e713.

5 (P3) — inclusiveDays built dates with Date.UTC

Confirmed, and daysBefore in the same file had the identical defect — it
returned "99-12-31" for daysBefore("0100-01-01", 1), unpadded as well as
nineteen centuries out. Both now build through setUTCFullYear, as
isIsoCalendarDate already did, and the ISO formatter pads the year to four
digits. customRangeError also rejects a range whose comparison window would
start before 0001-01-01, with a translated message. Fix 11d5d81.
Failing-first tests in dates.test.ts and layRateRange.test.ts.

Owner-added: the "All flocks" scope button on the dark card

Root cause found, and it is the #939 Tabs class exactly: Button defaults to
color="primary", which on the text and outlined variants is the FOREGROUND,
and palette.primary.main is raw --brand, which #149's dark palette blocks
never redeclare. Measured 1.18:1 on aubergine/dark against the card. Fourteen
call sites across seven screens take that default, so this was never one button.

Fixed in the theme with --stat-accent, the same accent MuiTabs and
MuiBottomNavigationAction already use. Measured per brand against the card
surface:

palette dark, before dark, after light (unchanged)
aubergine 1.18:1 9.35:1 13.78:1
forest 1.46:1 10.69:1 11.08:1
slate 1.42:1 10.15:1 11.44:1
terracotta 1.51:1 9.44:1 10.73:1

contained is deliberately untouched: there the brand is the background.

A false start worth recording. My first override used MUI v5's
textPrimary/outlinedPrimary slots. MUI v6 replaced those composites with
variants and separate MuiButton-text / MuiButton-colorPrimary classes, so
the override was dropped in silence — and my first policy assertion read the
override object, so it passed while the app was still broken. A capture caught
it. The assertion now resolves the colour the way MUI does, and
FarmThemeProvider.render.test.tsx renders a real Button and reads the DOM,
which is the check that cannot pass vacuously. Both were mutation-checked,
including a mutation that reinstates the v5 slot names. Fix 473e093.

Verification at 473e093

@mforce

mforce commented Sep 23, 2026

Copy link
Copy Markdown
Owner Author

Codex (gpt-6-sol) review round 2 of 473e093 — four P2s and one P3

Head is now 364ee441defb091135b727c55e1dc600b246d010. Every finding has a test
that was observed failing first, and every fix was mutation-checked.

1 (P2) — a duplicate-only page advanced the reader onto nothing

Confirmed. loadMore reported the cursor delta, which is what the SERVER sent,
and the hook deduplicates — so five newer orders shifting the offset made page 2
a repeat of page 1, reported as rows: 5, and the panel moved to "Orders 6 to 5"
with nothing on it.

loadMore now reports what the LIST gained, counted against the rows already
held, and consumes duplicate-only pages inside the same request until a page
adds something or the server runs short. The cursor still advances by what the
server returned, so the older records #465 exists to reach stay reachable — that
guarantee has its own test, updated to the new one-ask behaviour rather than
dropped. The walk is bounded so a list that is entirely duplicates cannot be
walked end to end on one tap.

Fix be53323. Failing-first Dashboard test: "keeps paging through a page of
rows it already has, and never lands on an empty one"
— offsets [0, 5, 10],
lands on "Orders 6 to 10", never renders "Orders 6 to 5". Two more at the hook
level pin the reported count and the bound.

2 (P2) — the drain ceiling was presented as an exact total

You are right and my round-1 reply was wrong. I checked the render rather
than my own claim: flocksTruncated reached only the Lay rate card's own
caption. Morning collection's "N of M houses in", its progress bar and its pager
all read the drained array's length as the farm's size, and a truncated ENTRY
list was not tracked at all — so houses past that ceiling would read as missing
with their eggs left out of the day's total, exactly as you describe.

The panel now carries an explicit incomplete state, in en, es and tl: the caption
reads "0 of 10000+ houses in", the pager "Houses 1 to 8 of 10,000+", a line under
the total says "Showing the first 10,000 houses. This farm has more.", and the
progress bar drops its value entirely — a share of an unknown whole is not a
share. No server-side paging and no summary endpoint.

Fix be53323. Two failing-first tests: the flock ceiling, and the entry ceiling
on its own (the second was added after a mutation proved the first did not cover
it). A third pins that a farm that finished draining still states an exact total.

3 (P2) — captureTiles scanned the entries per flock

Confirmed. One pass now indexes entries by flock id, then one lookup per flock.
The #82 rule is preserved exactly: Voided rows are skipped while indexing, and
the first non-voided row for a flock wins, which is the order a scan found it in.

Fix be53323. Test: 2,000 flocks joined to 2,000 entries with literal expected
results — 667 missing, missing-first, f0/f1998/f1/f1999 at known
positions, and the flock carrying a Voided row ahead of a real one resolves to
the Submitted one. Mutation (index keeps the Voided row) is red.

4 (P2) — abandoned drains kept walking the farm

Confirmed. Cleanup only guarded the final state update. listFlocks and
listDailyEntries now take an AbortSignal (apiGet already accepted one), the
effect aborts on cleanup, and drainPages checks the signal between pages — so
both the request in flight and the rest of the walk stop.

Fix be53323. Failing-first test: unmount after the first page is issued, land
that page, and assert the requested offsets are exactly [0].

5 (P3) — the large-farm test had no entries

Confirmed. Added a 501-flock, 501-entry case asserting "501 of 501 houses in",
entry offsets [0, 500], a day total of 577, and the 501st house — reached
through the pager — rendering its 77 eggs rather than "Not recorded". Mutation
that puts the entry read back to a single 500-row request turns it red, which is
the regression you named.

Verification at 364ee44

  • Typecheck clean; full web suite 3461 passed / 141 files.
  • Full quick Playwright suite on a rebuilt isolated cw914a stack, both
    projects: 96 passed, 1 intentionally skipped, 0 failed.
  • Eight mutations red across the five findings, listed above.
  • pstack:deslop re-run over the whole branch.

One contract test moved with the signal: listFlocks.test.ts asserted whole
apiGet calls, so an added optional argument broke assertions that exist to pin
the URL. They now read the requested path.

Screenshots — posted frames are at 473e093, not at the head

I recaptured every after-frame at 364ee44 on a freshly seeded stack and looked
at them: layout is identical to the posted set, with only regenerated order
reference numbers differing. GitHub is not currently serving newly uploaded
attachments on this PR
— every asset minted in the last hour returns 404 to
both anonymous and token-authenticated requests, including single-image probes,
while assets uploaded earlier today still return 200. So the four screenshot
comments have been left pointing at the verified-live 473e093 set and say so.

Nothing round 2 changed is visible at rest on those screens: the incomplete-state
notice needs a farm past 10,000 houses, and the duplicate-page handling is
behaviour rather than pixels. I will re-upload and re-patch once GitHub serves
attachments again.

@mforce

mforce commented Sep 23, 2026 •

Copy link
Copy Markdown
Owner Author

Codex (gpt-6-sol) review round 3 of 364ee44 — three P2s and two P3s

Head is now e051bf09cecb85d3eff7676871011fd1cc706521. Every finding has a test
observed failing first, and every fix was mutation-checked.

1 (P2) — a partly new page let the next tap skip unseen rows

Confirmed, with the interleaving exactly as described. The rule was "advance if
the list gained a row", which is wrong when the page in view is the one that
gained it: page 2 held only order 6, the next tap appended 7–10 onto that same
page and moved to page 3.

The rule is now: advance only when the page in view was already full before
the request. A short page keeps the reader on it and fills. One consequence
worth stating — the extra "does the destination hold a row?" check I first wrote
is redundant and is gone: a full page in view means the list already reaches
that page's last index, so any appended row lands past it. Its mutation stayed
green, which is how I found it was dead.

Fix e9e220a. Failing-first two-tap test: tap one lands on "Orders 6 to 6";
tap two fills that page to "Orders 6 to 10" and shows SO-5 through SO-9, instead
of stepping to page 3.

2 (P2) — Retry ran outside the effect's abort and generation guard

Confirmed. Retry minted a controller it never aborted and never checked whether
its answer still belonged to the current role.

Rather than copy the guards, Retry now bumps a generation the panels effect
depends on
, so there is one owned request path with one abort and one
cancellation check. The cost is that Retry re-reads entries and stock as well as
flocks; that is two extra reads on a rare, explicitly requested action, and it
recovers the whole panel set rather than one list. fetchFlocks is gone, along
with the two comments that still named it.

Fix e9e220a. Two failing-first tests: a role change during Retry (the late
answer must not overwrite the new role's list), and unmount during Retry
(requested offsets stay [0, 0] — the failed first load and the retry's first
page, nothing after).

3 (P2) — an entry-only ceiling disowned a house count we had

Confirmed, and the 40-flock test I added last round encoded the wrong
expectation. The two gaps are now separate:

  • a truncated flock list means the farm's size is unknown, so the count,
    pager and progress bar become lower bounds;
  • a truncated entry list leaves the house count exact and marks the
    status side incomplete instead, with its own line: "Some entries could not
    be read, so today's status and total cover part of the farm."

The morning brief no longer claims "Every house has an entry today" while
entries are incomplete; it says what it actually knows. Fix e9e220a. The
40-flock test now expects "0 of 40 houses in" with an exact progress value, and
a new test pins the brief.

4 (P3) — exactly 10,000 flocks claimed more existed

Confirmed. I took the wording fix, not the probe. Twenty full pages prove
"at least 10,000", never "more than 10,000", and wording that says only what was
proved costs nothing, adds no request, and is exactly true at 10,000 and at
10,001 alike. The probe would buy an exact answer for one farm size that cannot
occur, at the price of a request in the one case already under strain. So:
"of at least 10,000", "0 of at least 10000 houses in", and "Showing the first
10,000 houses. There may be more." Fix e9e220a, mutation on the old wording is
red.

5 (P3) — the abort tests never reached fetch

Confirmed. Three assertions now cover the chain: listFlocks and
listDailyEntries each hand their signal to apiGet, apiGet hands it to
fetch, and an abort rejects a request that is still open (a hung-server mock
that only the signal can end). Fix e9e220a. Removing signal forwarding from
either list client, or from apiGet, turns these red — all three mutations run.

On the ceiling handling, since you asked

It is close to the point of not being worth it. Three release-valves now hang off
one pathological case: a lower-bound count, a separate entry-incomplete state,
and the brief's own wording. None of it is reachable below 10,000 houses.

The simpler alternative, if you want it, is to delete the ceiling entirely and
drain until the server runs out
— the loop already terminates on a short page,
and the ceiling exists only to bound a pathological farm. That removes
housesIncomplete, entryDataIncomplete, three i18n keys per locale and four
tests, at the cost of an unbounded request count on a farm nobody has. I have
not done it: it is a behaviour change you did not ask for, and the current code
is correct. Say the word and it is a small deletion.

Verification at e051bf0

  • Typecheck clean; full web suite 3469 passed / 141 files.
  • Full quick Playwright suite on a rebuilt isolated cw914a stack, both
    projects: 96 passed, 1 intentionally skipped, 0 failed.
  • Eleven mutations red across the five findings, including the two that proved
    my own dead branch and my own wrong test expectation.
  • pstack:deslop re-run over the whole branch.

Screenshots — still at 473e093

I recaptured all 42 after-frames at this head on a freshly seeded stack, then
probed the upload path before touching any comment, as agreed. A single-image
upload still returns 404 on three tries over 30 seconds, while the assets already
on this PR all return 200. So the four screenshot comments are untouched and
still carry the verified-live 473e093 set with their own After frames captured at 473e093. line.

Nothing in rounds 2 or 3 is visible at rest on those frames: the incomplete
states need a farm past 10,000 houses, and the paging and cancellation fixes are
behaviour. I will re-upload and re-patch as soon as attachments serve.

@mforce

mforce commented Sep 23, 2026

Copy link
Copy Markdown
Owner Author

Codex (gpt-6-sol) review round 4 of e051bf0 — two P2s fixed, the P3 deferred

Head is now b512a9735a7b52758a3600f749c76cd82497817e.

1 (P2) — filling a short orders page told a screen reader nothing

Confirmed. Rows arrive ABOVE the pager, the page number does not move, and the
button keeps its name, so a reader standing on the control has nothing to go on.

I took the announcement, not the focus move: it is the smaller of the two
and it is the honest one here. The range is already the only thing on screen
that changes when a short page fills, so making it a polite live region needs
one element and no new control; moving focus into the list would take a reader
off the pager they deliberately walked to, and would need a ref onto a row that
only sometimes exists. The region is mounted with the pager and carries the
OLD text first — a live region that appears together with its own text is
announced unreliably, which this codebase has hit before.

Fix 38cb36f. Failing-first test: after the first tap the range node is
asserted to carry aria-live="polite" and aria-atomic="true" while reading
"Orders 6 to 6"; after the second tap the same node is asserted to read
"Orders 6 to 10", so the change is what gets announced rather than a fresh node
appearing. Both attributes are mutation-checked individually.

2 (P2) — a successful flock Retry waited on the other panels

Confirmed, including the reason it can hang indefinitely: fetch here carries
no timeout. Each panel now commits the moment its own read settles —
flocks, entries and stock each apply independently — while the page-level
"everything failed" verdict still waits for all three, because that verdict is
only true once nothing is outstanding. Round 3's single owned request path is
unchanged: one generation, one AbortController, one cancelled flag covering
all three reads.

Fix 38cb36f. Failing-first test: the first flock read fails, the reader taps
Retry, the flock read succeeds and the stock request never resolves. The
flock rows render, the panel error goes, and the Retry control is gone — with
stock still outstanding. A mutation that puts the flock result back behind the
batch is red, and so is one that drops the cancelled check, which keeps
round 3's role-change guarantee pinned.

3 (P3) — the entry-ceiling progress bar: deferred

Not fixed, by owner decision. The ceiling stays as it is, and the bar plus a
better design for the whole incomplete-data state are tracked in #942
(low priority). The PR body carries the same note so it is not lost at merge.

Verification at b512a97

  • Typecheck clean; full web suite 3471 passed / 141 files.
  • Full quick Playwright suite on a rebuilt isolated cw914a stack, both
    projects: 96 passed, 1 intentionally skipped, 0 failed.
  • Four mutations red across the two fixes.
  • pstack:deslop re-run over the whole branch.

Screenshots — now at the head

Attachment uploads are serving again. Following the order that went wrong two
rounds ago: probed a single upload first (200 on three tries), uploaded all 42
head frames, verified every one returns 200 before touching any comment,
then patched the four screenshot comments and re-verified all 50 posted images.
Each comment carries one After frames captured at b512a97. line, no stale
after-frame remains, and the assets still return 200 after the uploader comment
was deleted.

…swers

Codex gpt-6-sol review round 5 of b512a97: one pre-existing P2.
@mforce

mforce commented Sep 23, 2026

Copy link
Copy Markdown
Owner Author

Codex (gpt-6-sol) review round 5 of b512a97 — one pre-existing P2, fixed

Head is now 0b64395bc6d05531fa41d18ea67d992eae8a37ff. This is the last review
round on this PR, by owner decision.
Round 5 confirmed every earlier fix, and
its single finding was pre-existing on main rather than introduced here.

P2 — the first visit gated the whole screen on all three reads

Confirmed, and reachable without any Retry: getStock() carries no timeout, so
one stalled read held the full-page loading view forever and hid the panels that
had already answered, along with their errors and their Retry.

The gate now ends on the first read that settles, and each panel renders from
its own result. A panel that has not answered shows its own loading state rather
than the error of a read that has not failed — which needed telling "not here
yet" apart from "failed", so the entry and stock reads now record their own
failure the way the flock read already did. Those flags reset at the start of
every generation, so a request in flight is never painted with the previous
generation's failure.

What did not change: the page-level "everything failed" verdict still waits
for all three reads, because it is only true once nothing is outstanding. Round
3's single owned request path is intact — one generation, one AbortController,
one cancelled flag over all three reads.

Fix 0b64395. Three failing-first tests:

  • stock never resolves while flocks and entries succeed: the collection rows and
    the "1 of 1 house in" caption render, the full-page loading view is gone, and
    Stock shows its own progress rather than "Could not load.";
  • flocks and entries both fail while stock hangs: the Today panel error and the
    flock-list Retry are both on screen, and the page-level failure message is
    not, because one read is still outstanding;
  • a generation that follows a failed one shows the collection panel loading
    rather than inheriting the previous failure.

Six mutations red, including the two that first came back green and sent me to
write the third test: showing a pending collection read as a failure, and
dropping the per-generation reset of those flags.

Deferred, unchanged

The entry-ceiling progress bar (round 4's P3) stays as it is by owner decision
and is tracked in #942, referenced in the PR body.

Verification at 0b64395

  • Typecheck clean; full web suite 3474 passed / 141 files (3403 on
    origin/main).
  • Full quick Playwright suite on a rebuilt isolated cw914a stack, both
    projects: 96 passed, 1 intentionally skipped, 0 failed.
  • pstack:deslop re-run over the whole branch.

Screenshots

The change is only observable while a read is in flight, so nothing on these
frames differs at rest. I recaptured and re-posted them anyway now that uploads
are serving: probed first, uploaded all 42, verified every one returned 200
before patching, then patched the four comments, deleted the uploader and
re-verified all 50 posted images still return 200. Each comment carries one
After frames captured at 0b64395. line and no stale after-frame remains.

The drain regression test rendered 501 rows and clicked through 63 pages to
reach the last one, 3.6s locally and a timeout under CI's coverage gate. The
same property is proved with one rendered row.
@mforce
mforce merged commit e0c1f68 into main Sep 24, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant