Skip to content

feat(web): expand the Lay rate chart into an overview map and a scrolling daily window - #958

Merged
mforce merged 9 commits into
mainfrom
feat/941-expanded-lay-rate
Sep 26, 2026
Merged

mforce merged 9 commits into
mainfrom
feat/941-expanded-lay-rate

Conversation

@mforce

@mforce mforce commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Screenshots: before and after, 1280 and 390, light and dark — 39 frames, captured 1:1 from stacks built at the base and at this head.

Closes #941

Component plan. docs/designs/822-mui-revamp.md D2 pair 21 covers this screen: DayStrip, StockBar, GradingChip, BrandSplash and the rest are kept as they are, bespoke data marks whose CSS stays in styles.css. The expanded chart is that family's second surface. It reuses DayStrip's marks and keyboard model rather than redrawing them, and takes its frame from BrandSplash (§ "Stays for good"). MUI supplies the controls it does elsewhere: TextField select native for the range, FilterDateField for the custom dates, Button for the pager, Apply and the way out.

What changed

The chart. An Expand control on the Lay rate card opens the same chart full-screen, composed like the post-login splash: a fixed full-viewport backdrop, the content centred, one way out that takes focus on open, and a fade only under prefers-reduced-motion: no-preference. It follows the app theme rather than forcing a dark lightbox (owner, 2026-09-24). Inside it, an overview map of the whole range sits above a daily window that is a native overflow-x scroll region, with a box marking the days on screen.

The sync has one writer each way. Scrolling writes the box's style; a press on the map writes scrollLeft. A programmatic scrollLeft fires scroll, which repaints the box, and the box never writes back, so the chain terminates after one hop and needs no guard flag. Giving the box its own index state is what brings that loop back.

Ranges. The card keeps #940's 7/14-day ceiling. The chart offers the last 30 finished days or a custom span up to 90, remembered under its own storage key so opening it never moves the card's window, and read only while it is open, so a reader who never expands pays nothing for a 90-day report. Its custom form is held to its own ceiling, and rejects the 91st day naming 90, not 14.

Bars are 22px at every range (owner, 2026-09-24), never stretched. justify-content: flex-end is the whole of "the newest day sits at the right edge"; a range narrower than the window leaves its empty space before the oldest day.

lib/dayWindow.ts is the one oracle. A single pure function answers, from measured pixels, whether the range fits, which days are on screen, where the box sits and which edges hide something. The box, the two edge cues, the strip's tab stop and the "days shown" caption all read that one answer instead of each measuring the DOM their own way.

The four scope additions, each with a test

  1. Visible keyboard focus on a selected bar. It was not merely quiet, it was absent: .day.on is (0,2,0) and the global :focus-visible is (0,1,0), so a focused day drew the same ink ring a hovered one draws. The new indicator is two-toned because no single tone clears 3:1 against both neighbours — the accent measures 1.20–1.55 against the --ink a selected bar is filled with, in all four brands and both themes. DayStrip.focus.test.ts measures every adjacency of the chain (bar → surface tone → accent ring → card) for eight brand/theme pairs, and records the accent-against-ink figure that makes the second tone load-bearing rather than decorative. Fixed in the shared strip, so the card benefits too.
  2. Tabbing in keeps the window where it is. The single tab stop and the arrow keys' fallback now both resolve to the first day actually on screen, not day 0.
  3. "Fits" is decided from layout, days × slot + (days − 1) × gap against clientWidth, never from scrollWidth.
  4. No stray fade band. The cues live outside the scroll region. Inside it they are positioned against the scrolled content, which is what drew a pale band down the middle of the lab's own render.

Three defects the captured frames found, and the diff did not

  • The permanent Drawer paints at theme.zIndex.drawer (1200) and the overlay was at 60, so the sidebar covered the chart. It now stacks at MUI's modal layer.
  • The date rule's end label is laid out 22px into its slot and only painted back inside it by a transform, so its layout box hung past the strip: measured at 1280, a 41-day range that fits reported scrollWidth 1140 against clientWidth 1076, and the view opened scrolled past its own first bars. The rule is clipped now, and the region's scrollable width is the strip's and nothing else.
  • A shrunken flex panel drew the plot over the legend at 390 (a 320px strip in a 298px plot). The panel and the plot no longer shrink, and the phone plot is sized so the whole column fits.

Owner review round

The To date field was cramped at 390: 137px of box for a 92px date, 28px of padding and a 14px icon left the date three columns clear of the icon. Both dates now take the full control column with Apply under them, and the phone plot loses 32px so the Escape hint stays on screen. tools/simulation/ui/specs/expanded-range-fields.spec.ts measures the clear columns between the date and the icon — 3 on aa9d700, 84 now.

Decisions taken

  • The expanded strip drops the card's 9px week jog, keeping the hairline in the existing gap. Over 90 days that jog is more than 100px of drift the slot arithmetic does not model, and every figure dayWindow returns would carry it.
  • Behaviour is shared through useDayStrip + DaySlots, not a second strip, so the one tab stop, the arrow keys, the measured readout clamp and the three day states cannot differ between card and chart.
  • The expanded read needs only the plotted window. The hen-day comparison against the previous equal window is the card's measure and stays there.
  • docs/designs/941-expand-chart/ commits the lab and its renderer, not the C2-*.png renders: they are reproducible from that file by name and are already attached to web: expand the Lay rate chart into a centred overview + scrollable daily view for ranges beyond 14 days #941.

Tests

Measured on this branch before and after, never quoted.

files tests
web before (42c8916) 142 3522
web after 146 3575
file before after
lib/dayWindow.test.ts — 9
components/DayStrip.focus.test.ts — 18
components/ExpandedLayRate.test.tsx — 10
components/ExpandedLayRate.styles.test.ts — 5
lib/layRateRange.test.ts 17 22
routes/Dashboard.test.tsx 125 131

Playwright quick suite at the head: 109 passed in 5.3 min, both projects, against an isolated stack built from this branch (108 before, plus the new phone spec). dotnet test --filter "FullyQualifiedName~ImagePin|FullyQualifiedName~RealTree": 14 passed.

Mutation checks

Every guard shown failing on a mutation before the claim was written.

mutant what it changes result
fits-from-measurement never answers "fits" from layout RED, 3 tests
cue-inside-the-scroller moves both edge cues inside the scroll region RED
tab-stop-back-at-day-one tab stop falls back to index 0 RED, 2 tests
focus-ring-single-tone inner focus tone becomes --focus RED
focus-ring-ties-the-selection-ring .day.on:focus-visible → .day:focus-visible RED
slot-drifts-from-the-arithmetic .bigstrip --slot 22px → 20px RED
week-jog-back-in-the-strip restores margin-left: 9px RED
newest-day-no-longer-at-the-right-edge flex-end → flex-start RED
expanded-ceiling-lowered MAX_EXPANDED_RANGE_DAYS 90 → 60 RED, 5 tests
expanded-read-issued-before-it-is-opened drops the if (!expanded) return RED, 4 tests
focus-not-returned-to-expand drops expandRef.current?.focus() RED
one-memory-for-two-windows writes the expanded window to the card's key RED
the phone geometry check run on aa9d700, before the layout fix RED at 3 clear columns against 8

The last two are worth a note. one-memory-for-two-windows survived its first version of the test, which reopened the view without unmounting and so read React state rather than storage; the test now remounts. And cue-inside-the-scroller's first mutation unbalanced the JSX, so the file failed to parse — a compile error, not a guard catching anything. Both were rewritten until the mutation was genuine.

Deletions

None. No CSS class, component, export or i18n key was removed, so there is nothing to grep for. DayStrip's internals moved into useDayStrip/DaySlots with its public props unchanged; its 15 existing tests pass untouched.

Docs

specs/product/GLOSSARY.md gains Expanded lay rate chart (#941) and the dashboard blurb names Expand; the in-app glossary gains the matching entry and the Help page's Dashboard section gains a paragraph — all three in en, es and tl, each using that locale's own control labels (#688): Expand/Ampliar/Palakihin, Range/Período/Saklaw, Custom range…/Período personalizado…/Sariling saklaw…, Back to dashboard/Volver al panel/Bumalik sa dashboard.

Found, not fixed

  • HelpPage.test.tsx asserted getByText(/always a/i) for a sentence about whole numbers; any later prose containing "always at" made it ambiguous. Tightened to /it is always a/i, which is what its own comment says it is for.
  • The native range select truncates its longest option at 390 (Período personalizado..). Pre-existing on the card too, untouched here.
  • readme-farm's depleted third flock makes most days before 27 August read as partial, so the 60- and 90-day frames are hatched. Correct behaviour, not a defect, but it is why the long ranges look busier than a three-live-house farm would.

…ling daily window

Concept C of the #941 mockup lab, chosen by the owner on 2026-09-24.

Closes #941
…t off the arithmetic

Three defects found by looking at the captured frames rather than the diff:
the permanent Drawer painted the sidebar over the overlay; the date rule's end
label added 64px of scroll the chart had no days for; and a squashed panel put
the plot on top of the legend at 390.
Side by side at 390 the To field had 137px of box for a 92px date, 28px of
padding and a 14px picker icon, so the date ended three columns short of the
icon. Both dates now take the full control column with Apply under them, and
the phone plot loses 32px so the Escape hint stays on screen.

specs/expanded-range-fields.spec.ts measures the clear columns between the
date and the icon; it reads 3 on aa9d700 and 84 here.
@mforce

mforce commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner Author

Screenshots

Captured 1:1 on readme-farm, from isolated stacks built for this PR: before frames from a stack built at the base 42c8916, after frames from one built at this branch's head. Same farm, same data, same scenario. Every frame is viewport-only at device scale 1.

After frames captured at 1986080.

The collapsed card gains an Expand control

before (42c8916) after
1280 light Before, card at 1280 light After, card at 1280 light
1280 dark Before, card at 1280 dark After, card at 1280 dark
390 light Before, card at 390 light After, card at 390 light
390 dark Before, card at 390 dark After, card at 390 dark

The card itself is unchanged apart from the control: it keeps #940's 7/14-day range and its own remembered window.

The expanded chart, by range

Net-new surface, so these are after-only. 30 days is the preset the view opens on; 60 and 90 are custom ranges. Each opens with the newest day at the right edge.

30 days 60 days 90 days
1280 light 30 days at 1280 light 60 days at 1280 light 90 days at 1280 light
1280 dark 30 days at 1280 dark 60 days at 1280 dark 90 days at 1280 dark
390 light 30 days at 390 light 60 days at 390 light 90 days at 390 light
390 dark 30 days at 390 dark 60 days at 390 dark 90 days at 390 dark

At 1280 a 30-day range fits: the bars stay 22px and the empty space goes on the left, the pager is disabled and the box covers the whole map. At 390 nothing fits, so the map is always live.

The window moving, and the box with it

The same 90-day range scrolled to the start and to the middle. The box tracks the window, and the edge cues light only at an edge that really hides days.

at the start mid-range at the end
1280 light 90 days at the start, 1280 light 90 days mid-range, 1280 light 90 days at the end, 1280 light
1280 dark 90 days at the start, 1280 dark 90 days mid-range, 1280 dark 90 days at the end, 1280 dark
390 light 90 days at the start, 390 light 90 days mid-range, 390 light 90 days at the end, 390 light
390 dark 90 days at the start, 390 dark 90 days mid-range, 390 dark 90 days at the end, 390 dark

A day's readout, and keyboard focus on it

The focus frames are the #941 scope addition: the ring is two-toned, so the accent never has to be read against the --ink the selected bar is filled with. Zoom them.

pointer readout keyboard focus
1280 light Selected day readout, 1280 light Keyboard focus on a selected bar, 1280 light
1280 dark Selected day readout, 1280 dark Keyboard focus on a selected bar, 1280 dark

A range that just fits, and the long locale

frame
41 days at 1280, light — no scroll, no edge fade, pager disabled, "All 41 days shown" A 41-day range that just fits, 1280 light
90 days mid-range at 1280, tl 90 days mid-range, 1280 tl

The custom-range dates at 390

Both dates take the full control column with Apply under them. Side by side, the To field had 137px of box for a 92px date, 28px of padding and a 14px picker icon, leaving the date three columns clear of the icon.

after
390 en light Custom range form at 390, en light
390 en dark Custom range form at 390, en dark
390 es light Custom range form at 390, es light
390 tl light Custom range form at 390, tl light
390 tl dark Custom range form at 390, tl dark

tools/simulation/ui/specs/expanded-range-fields.spec.ts measures the clear columns between the date and its icon: 3 on aa9d700, 84 here.

Review round 1 changed no pixels at either captured viewport — its fixes are modality, focus, announcements and arithmetic. These frames are the head's own all the same; the largest mean per-pixel difference against the round-0 set is 1.0/255, which is font rasterisation between runs.

No console errors or CSP refusals were observed on any capture run.

…ff the strip

Round 1 of the #958 review (Claude Fable 5.1). The frame is a MUI Modal now,
so siblings go aria-hidden, the body's scroll is locked and restored, Escape
closes and Tab is contained however focus got where it is. The hand-rolled Tab
and Escape handlers are gone and so is the literal z-index.

Also: the gap comes off the strip rather than a JS constant, so the two media
queries cannot disagree at exactly 900px; the re-anchor effect answers to the
report; Page keys are left to a date field; a pager that disables hands focus
to its neighbour; the window is announced once it settles; and the map claims
only the horizontal axis where a phone cannot drag it.
Thirty days fits on either gap at that width, so the first version of this
passed on the pre-fix build. Thirty-two is inside the window where the
laid-out 2px gap fits and a JS-side 4px does not.
@mforce

mforce commented Sep 26, 2026

Copy link
Copy Markdown
Owner Author

Review round 1 (Claude Fable 5.1, at 7f338f0) — all seven findings taken

Head is now 1986080f. Fixes in 1c920a13, comment pass in eb86ada5, one test correction in 1986080f.

Every claim below was shown failing first. The two P2 browser checks and the 900px check were run against a stack built from web/src at 7f338f0 itself; the rest against a mutation of the fix on the current head.

P2 · Modality was claimed and not enforced

Fix 1c920a13. The frame is a MUI Modal now (hideBackdrop, with .lay-expand-backdrop as its child), so it reuses the ModalManager components/Dialog.tsx already depends on. Siblings go aria-hidden, the body's scroll is locked and restored, Escape closes through onClose, and Tab is contained however focus got where it is. The hand-rolled Tab and Escape handlers are gone, and so is the literal z-index: 1300 — the frame takes theme.zIndex.modal. overscroll-behavior: contain on .lay-expand and .lay-expand-scroll stops a vertical pan chaining to the document, which is what let pull-to-refresh fire.

Focus still returns to Expand, but from a different place: called inside onClose it ran while the frame was still mounted, whose trap pulled it straight back and then dropped it on <body> at unmount. It is an effect on the Dashboard now, after the overlay is gone. Opening focus is FocusTrap's, via data-mui-focusable on the way out.

Verification, all four of the ones you named:

claim where on 7f338f0
siblings are aria-hidden while open, and are not after ExpandedLayRate.test.tsx red
the body's scroll is locked, and restored same red
Tab stays inside after a click on the backdrop specs/expanded-chart-modality.spec.ts red — focus left on Tab 1
Escape closes from anywhere inside, focus returns to Expand ExpandedLayRate.test.tsx, Dashboard.test.tsx red

The Playwright spec also drives a real wheel over the backdrop and asserts window.scrollY does not move, then that it does again after close. On 7f338f0 the body's computed overflow was visible.

One consequence worth naming: with the page behind genuinely out of the accessibility tree, two Dashboard tests that read the card through an open chart could no longer see it. They close the chart first now. That is the fix working, not a workaround.

P3 · JS and CSS disagree at exactly 900px

Fix 1c920a13, taking your second option. ExpandedLayRate reads columnGap back off the strip, so the stylesheet is the only thing that decides it and no query can disagree with a constant. DAY_GAP_PHONE_PX had no production reader left and is deleted; DAY_GAP_PX stays as the fallback for jsdom, which loads no stylesheet. ExpandedLayRate.styles.test.ts now asserts the two rows narrow together rather than pinning the phone value to a constant nothing reads.

Failing first at 7f338f0, in specs/expanded-chart-modality.spec.ts: at 900px the strip lays out inside its region and the pager was still live. My first version of that test used the 30-day preset and passed on the broken build — 30 days fits on either gap at that width. It is pinned to 32 days now, inside the window the two answers actually disagree over (1986080f).

P3 · Crossing 900px while open jumps to the newest day

Fix 1c920a13. The effect is keyed on data. Two notes. The gap fix already removed gap from sync's identity, so the mutation you would expect here survives — the cause was gone. What [data] earns on its own is a new range of the same length: keyed on anything derived from the day count, a 30-day preset swapped for a 30-day custom range leaves the reader at the wrong end of a range they did not ask for. That is the test (re-anchors on the newest day when a new range of the same length arrives), red under }, [days]. Dashboard.tsx memoises expandedLine so the identity means what it says.

P3 · PageUp/PageDown cancelled inside the date fields

Fix 1c920a13. The handler returns early when the event target is an input or select. Red on removing that line.

P3 · A pager button disabling under focus drops focus to <body>

Fix 1c920a13. scrollLeft reads back clamped in the same tick, so the button knows it has reached the edge before the disabled attribute commits and hands focus to its neighbour. Red on routing the click back to the plain page().

P3 · The visible range is never announced

Fix 1c920a13. Split rather than made live in place: the visible .lay-expand-shown is aria-hidden and never lags the chart, and a permanently mounted .sr-only aria-live="polite" sibling carries the same sentence on a 500ms settle. A drag crosses a day boundary every 26px, so an undebounced region would speak about forty times on one sweep of a quarter; splitting them is also what keeps the sentence from being read twice. Red on removing aria-live.

P3 · touch-action: none kills vertical swipes on the map

Fix 1c920a13. pan-y in the phone rule; the desktop rule keeps none because that is where the box is dragged. Red on putting none back.

Your notes on tests and slop

  • The wrong comment in ExpandedLayRate.test.tsx (41 days against a 2,340px viewport) is corrected, and the .scroll-cue comment no longer claims a token the rule does not use.
  • DayStrip.focus.test.ts's accent-against-ink assertion: I have left it, and I think you are right. It pins a palette weakness, so a future palette that clears 3:1 turns it red for improving. I did not change it because it was not in the seven the coordinator accepted and dropping an assertion is their call, not mine. If you want it gone the figure belongs in the comment above the CSS rule; say so and it goes in round 2.
  • The duplicated expandMapLabel is now genuinely load-bearing in one direction only — the visible label and the map's aria-label. Left as is.
  • Unrelated but found while here: tools/simulation/ui had five tsc errors under noUncheckedIndexedAccess in the spec I added last round. npm test there runs Playwright, not tsc, so nothing was catching them. Fixed in 1c920a13.

Verification

npm test in web/: 3587 passed, 146 files (3575 before this round). Playwright quick suite against a stack built at this head: 112 passed in 5.7 min, both projects, inside the 10-minute PR limit. Typecheck clean in both projects. Screenshots recaptured at 1986080f and the screenshot comment patched in place; this round changed no pixels at either captured viewport, and the largest mean per-pixel difference against the round-0 set is 1.0/255.

…and its quiet

Round 2 of the #958 review (Claude Fable 5.1). Opening focus comes from the
button's own ref callback instead of data-mui-focusable, which had made Close
the focus trap's focusTarget and its last tabbable node at once, so every
Shift+Tab focused it again. The frame is counted through Dialog.tsx's own
open-count, so #485 still records the debt when a PWA update banner mounts
behind it. The live region no longer speaks for a window nobody moved, and the
document's own overscroll is held off at the root, where the scroll chain
actually is.

Also, by owner decision: DayStrip.focus.test.ts no longer asserts the accent
sits BELOW 3:1 against --ink. That pinned a palette weakness and would have
gone red for an improvement; the figure is recorded in the stylesheet.
Shift+Tab from the one way out was a dead key while it carried
data-mui-focusable; forward Tab happened to work, so the existing spec could
not see it.
@mforce

mforce commented Sep 26, 2026

Copy link
Copy Markdown
Owner Author

Review round 2 (Claude Fable 5.1, at 1986080) — all four findings taken

Head is now a9515d35. Both P2s were mine from round 1, which is the point: the Modal fix bought real modality and broke two things underneath it.

Every claim below was shown failing first. The Shift+Tab case ran against a stack built from web/src at 1986080 itself; the other three against a mutation of the fix on the current head.

P2 · Shift+Tab from Close was a dead key

Fix b23b2a98, as you prescribed, with one correction to the mechanism. data-mui-focusable is gone and the backdrop carries tabIndex={-1}. But restoring the closeRef mount effect did not work: Modal renders its children into a portal on a second commit, so the ref is still null when this component's own mount effect runs — FocusTrap then focused the frame's root, and the opening-focus assertion caught it. Opening focus comes from the button's own ref callback instead, which runs inside the portal's commit, before the trap's effect; the trap then leaves focus alone because it is already inside the root. Same end state, and it survives the extra commit.

Failing first at 1986080, in specs/expanded-chart-modality.spec.ts: Shift+Tab focused Close again — expected false, received true. The new case asserts focus lands on the day strip's single tab stop, not merely that it moved.

P2 · The bare Modal bypassed the open-count

Fix b23b2a98. Dialog.tsx exports useCountedAsOpenDialog() — mount +1, unmount -1, through the same bumpOpenCount — and ExpandedLayRate calls it. I took the hook rather than rendering through the repo's Dialog: this frame is the splash's composition by owner decision, and wrapping it in a dialog shell to borrow one counter would have put the chrome back that #941 deliberately does not have.

Two tests, both red on removing the call: anyDialogOpen() is true while the chart is mounted and false after, and — the one you asked for if practical — an update banner raised while the chart is open is held silent and replayed after close, driven through useMissedAnnouncement itself rather than through the counter directly.

P3 · The live region spoke on open and on every range change

Fix b23b2a98, your recipe. announced starts empty, a settled ref resets to null on every data change, and the timer writes only when there is a previous settled reading and it differs. So the first settled sentence of a range is the baseline, not news. The test now asserts silence after opening and after the first settle, and the sentence only once a scrolled window settles — red on collapsing the timer back to a plain setAnnounced.

P3 · overscroll-behavior: contain on .lay-expand is inert where the gesture lives

Fix b23b2a98. You are right that a container with no overflow is never in the chain, and .lay-expand is sized to fit at both captured viewports. An effect sets document.documentElement.style.overscrollBehaviorY = "contain" while the chart is open and restores the previous inline value on close. I did not take the app-wide html rule: turning pull-to-refresh off for the whole app is a product decision beyond this slice, and worth its own issue if you want it. The rule on .lay-expand stays — it is live on a short phone or a long locale, where the column does overflow — but its comment no longer claims to be the mechanism. Red on dropping the root line.

Owner decision folded in

DayStrip.focus.test.ts no longer asserts that --focus sits below 3:1 against --ink. You called it in round 1 and the coordinator agreed: it pinned a palette weakness and would have gone red for an improvement. The measured figure (1.20–1.55, all four brands, both themes) is recorded in the comment above .day.on:focus-visible instead. Every remaining assertion in that file is a minimum, and the eight adjacency checks are untouched — which is what made the two-toned ring necessary in the first place.

Verification

npm test in web/: 3582 passed, 146 files. The count is down from 3590 because the eight deleted assertions above were per brand per mode; three new tests landed in this round. Playwright quick suite against a stack built at this head: 113 passed in 5.3 min, both projects, inside the 10-minute limit. Typecheck clean in both projects.

Screenshots are unchanged and were not re-uploaded. Round 2 touches focus, a counter, a live region and the root's overscroll — nothing that paints. Checked rather than assumed: eight probe frames recaptured at this head (the open chart at 1280 and 390 in both themes, plus the readout and keyboard-focus frames) are byte-identical to the ones on the PR, mean and max difference both 0.

@mforce

mforce commented Sep 26, 2026

Copy link
Copy Markdown
Owner Author

CI note: one failure on this head, and it is not this PR's

The first run of Web typecheck, test, and build at a9515d35 failed on
DailyEntryPage.test.tsx > DailyEntryPage new-flock role-change session (#703 r2) > does not retarget after an in-flight create survives an admin demote+re-promote.
Re-run, the job is green; everything else was green first time.

That file is untouched by this branch, and the test passes locally three times
in a row. It is a pre-existing flake: the same test failed on main in
run 36056126989
on 2026-09-24, before this branch existed.

Why it is load-sensitive is visible in the test's own comment: the assertion
expect(screen.queryByRole("textbox", { name: "Flock" })).not.toBeInTheDocument()
holds only because MUI's modal marks the rest of the page aria-hidden while
the new-flock dialog is open, which is what takes the trigger out of the
accessible tree. It is asserting a side effect of ModalManager's timing
rather than the thing it is named for, so a slower runner can observe the
window before that attribute lands.

Not fixed here — it is outside this slice and the fix is a judgement about what
that test should assert. Reported rather than re-run quietly.

@mforce

mforce commented Sep 26, 2026

Copy link
Copy Markdown
Owner Author

Claude Fable 5.1, review round 3 at a9515d3: no P1, P2 or P3 findings. It confirmed every round-2 fix, including a StrictMode probe the PR tests do not cover. The review loop ends here.

@mforce
mforce merged commit 852c845 into main Sep 26, 2026
26 of 27 checks passed
@mforce
mforce deleted the feat/941-expanded-lay-rate branch September 26, 2026 01:49
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.

web: expand the Lay rate chart into a centred overview + scrollable daily view for ranges beyond 14 days

1 participant