Skip to content

fix(dashboard): give recent sales real columns and make both charts readable - #781

Merged
mforce merged 6 commits into
mainfrom
fix/777-dashboard-panels
Sep 12, 2026
Merged

mforce merged 6 commits into
mainfrom
fix/777-dashboard-panels

Conversation

@mforce

@mforce mforce commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner

Closes #777

Design approved on the issue: mockups and charts, second pass.

Screenshots

Captured from this branch's head against the dev stack (Compose Postgres, dotnet run API, Vite), at 1:1, no downscaling. Same farm, same data, same scenario in every pair.

Dashboard at 1440px — before / after

Dashboard before, dark
Dashboard after, dark
Dashboard before, light
Dashboard after, light

Recent sales in a panel wide enough for four columns — before / after

Recent sales before, wide panel
Recent sales after, wide panel

Recent sales at phone width — before / after

Recent sales before, phone
Recent sales after, phone

Recent sales

The tracks are sized on the <ul> and each <li> is a grid-template-columns: subgrid row of it, so the columns align down the list rather than per row. The <li> keeps its own box, so the hairline and the aria-label are untouched — which display: contents would have cost. max-content on the badge and amount tracks measures whatever the farm's locale renders, so nothing is sized from English (#688). The reference number gets white-space: nowrap; the customer name truncates rather than wrapping, and the clip is visual only — the link's accessible name is still the whole name.

The narrow case is a container query, and that is load-bearing rather than tidy. Measured on the running app, the sales panel is 331px wide at a 1440px viewport and 863px at a 900px one, because .dash-grid rewraps. A viewport media query — which is what I wrote first — would have handed the desktop case four tracks in a 331px panel and collapsed the name column to nothing. Both branches render in the real app; neither is dead CSS.

Last 14 days

Corrected after review. This paragraph originally said the line "drew a segment straight through days with nothing recorded, so a stretch nobody had entered read as a plateau of real production". That is false: sparkline mapped a zero day to y = SPARK_H, the floor of the viewBox, so a zero was drawn as a drop — which is what the before screenshot shows. The claim is corrected below and in the four source files that carried it. See the review round.

A strip of day slots, not a line. What the line failed to do was draw either end of its scale, so nothing on screen said the bottom meant zero rather than the window's own minimum, and a 3% swing and a 60% swing made the same picture. It also interpolated between days, implying values that a daily count does not have. One slot is drawn per day whether or not that day has a figure, and both ends of the scale are named. It does not disambiguate a zero day from an unrecorded one; that is #780.

Bars are anchored at zero and sized as a share of the peak. Not a cropped axis: a steady farm should produce near-equal bars, and shortening the axis to dramatise a small swing would misstate the ratios between days. The week boundary the caption compares across is one hairline. Hen-day is now a figure with a semantically coloured delta; it was the smallest text on the panel, inside a muted sentence describing a quantity the chart above did not plot.

No average reference line, deliberately. Averaging the recorded days asserts the flat days are absences; averaging the whole window asserts they are real zeros. That is #780, so a reference line here would quietly pick a side.

Stock

Corrected after review. The hues below were retuned in 38d9ac0: three of them sat within dE 19 of --success, --warn, --error or a farm palette's accent, and the hue was assigned from the post-filter index, so a grade selling out recoloured the rest.

Grade was encoded by opacity alone, on a ramp that hit its 0.35 floor at the sixth grade and gave a seventh and eighth literally the same fill. It is categorical now: eight hues per theme, cycling past eight with the ledger doing the naming. The hues are deliberately outside the farm-palette system — a farm's palette identifies the farm, these identify grades, and a grade that changed colour with the deployment's palette would make two farms' screenshots uncomparable. styles.grades.test.ts holds that, along with distinctness, a 3:1 floor against the panel, and declaration in each mode's own base block.

The wheel opens blue, violet, green on purpose. It opened brick, ochre, green in the first draft, which made a farm's Small, Medium, Large read as a red/amber/green ramp — the interface saying Small is bad.

The total leads, and the ledger carries each grade's name, count and share, so Dirty at 0.6% is readable as a number even though its band is a sliver. The ledger is the bar's accessible text of record and the track stays aria-hidden="true", so decision E's requirement — replace the text of record, never just remove it — holds.

Guards

styles.grades.test.ts is new and was mutation-checked before it was trusted. Five of six mutations went red first time; the sixth did not, and that is the interesting one.

mutation result
duplicate a hue (--grade-7 := --grade-1) red
a brand palette redeclares --grade-3 red
GRADE_COLOURS 8 → 9 with no ninth token red
a hue that vanishes on its surface red
drop --grade-8 from the dark base green — guard was wrong
drop --grade-8 from the dark base, after the fix red

The dark base is :root[data-theme="dark"], so a token it omits resolves to the light value rather than to nothing — a dark hue left on a dark panel, with distinctness and contrast both still passing. Same trap DARK_REQUIRED in styles.test.ts exists for. The guard now asserts declaration in each mode's own block, not just resolution.

Two existing guards were taught rather than worked around:

  • Login.styles.test.ts fails closed on any at-rule context it does not model, and @container was one. It is @media's sibling for that guard's purpose — both wrap a rule in a condition that holds at some size — so the floor inside one has to meet the bar exactly as an unconditional rule does.
  • styles.test.ts's colour-token check matched border(-[a-z]+)?, which catches border-radius. That is a defect in the guard, not a missing token: it has been treating every radius as a colour and passing only because the values happened to be tokenised. Narrowed to the properties that can carry a colour.

Callers

tools/simulation/ui/specs-screenshots/screenshots.spec.ts asserted the sparkline's polyline had more than one distinct y. Updated to assert the bar heights differ, and not the slot count — fourteen slots are now drawn whatever the figures, so counting slots would pass on a report that never arrived. specs/owner.spec.ts selects .meter-stack > span and .dash-list li, both unchanged.

No API change, so no write-contract callers (#394). No user-visible concept changes, so specs/product/GLOSSARY.md and the Help page are deliberately untouched.

Verification

npx vitest run — 124 files, 2934 tests, green. npm run typecheck and npm run build clean. Driven in a real browser on the dev stack at 360/420/560/700/820/900/1024/1200/1440/1800/2400/3200px; no page errors beyond the dev server's own service-worker MIME warning and the pre-login 401 probe.

…eadable

Recent sales rendered four fields per row with only the amount positioned, so
nothing but the amount formed a column. The rows are now a grid whose tracks
are sized on the list and subgridded per row, so the columns align down the
list; the reference number stops wrapping mid-identifier, the customer name
truncates rather than wrapping to three lines, and a panel too narrow for four
tracks folds to two lines on a container query rather than a viewport one.

Last 14 days was a line drawn straight through days with nothing recorded, so a
stretch nobody had entered read as a plateau of real production. It is a strip
of day slots now: one slot per day whether or not that day has a figure, bars
anchored at zero and sized off the peak, the week boundary the hen-day figure
compares marked, and hen-day itself promoted from the smallest text on the
panel to the figure it is.

Stock encoded grade by opacity alone, which reached its floor at the sixth
grade and gave a seventh and eighth the same fill. Grade is categorical now:
eight hues per theme, deliberately outside the farm-palette system so a grade
is not a different colour on two deployments, with a ledger beside the bar
carrying each grade's name, count and share. The ledger replaces the grade
run-on the caption used to carry and is the bar's accessible text of record.

Presentational only. No API change, no change to what either panel shows or to
who can see it.
@coderabbitai

coderabbitai Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 8833c931-0dd2-48d0-b023-2c291f59f476


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mforce

mforce commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Presentational change to three Dashboard panels: a subgrid column layout for the recent-sales list, the 14-day line replaced by a day strip, and the stock bar moved from an opacity ramp to a categorical hue set with a ledger.

Worth a close look at three things. .panel:has(.dash-list) becomes a container and the list's narrow layout is a @container query rather than a viewport one — the panel is 331px wide at a 1440px viewport, so the two do not agree. styles.grades.test.ts is a new guard over the --grade-N tokens, including that each mode's base block declares them rather than inheriting. And two existing guards were changed rather than worked around: Login.styles.test.ts now accepts @container alongside @media, and styles.test.ts's colour-token check no longer treats border-radius as a colour property.

@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- changed files ---'
git diff --name-status "$(git merge-base HEAD origin/main)" HEAD || git diff --name-status HEAD~1 HEAD

printf '%s\n' '--- candidate files ---'
fd -t f . | rg '(^|/)(Dashboard|Login\.styles\.test|styles\.grades\.test|styles\.test)\.(tsx|ts|css)$' || true

printf '%s\n' '--- dashboard-related diff stat ---'
git diff --stat "$(git merge-base HEAD origin/main)" HEAD || true

Length of output: 2560


🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="$(git merge-base HEAD origin/main 2>/dev/null || git rev-parse HEAD~1)"

printf '%s\n' '--- changed files ---'
git diff --name-status "$base" HEAD

printf '%s\n' '--- candidate files ---'
fd -t f . | rg '(^|/)(Dashboard|Login\.styles\.test|styles\.grades\.test|styles\.test)\.(tsx|ts|css)$' || true

printf '%s\n' '--- dashboard-related diff stat ---'
git diff --stat "$base" HEAD

Length of output: 2535


🏁 Script executed:

#!/bin/bash
set -euo pipefail
base="$(git merge-base HEAD origin/main 2>/dev/null || git rev-parse HEAD~1)"

printf '%s\n' '--- outlines ---'
ast-grep outline web/src/routes/Dashboard.tsx --items all
ast-grep outline web/src/components/DayStrip.tsx --items all
ast-grep outline web/src/components/StockBar.tsx --items all
ast-grep outline web/src/lib/dashboard.ts --items all

printf '%s\n' '--- application component and model diff ---'
git diff --unified=80 "$base" HEAD -- \
  web/src/routes/Dashboard.tsx \
  web/src/components/DayStrip.tsx \
  web/src/components/StockBar.tsx \
  web/src/lib/dashboard.ts

printf '%s\n' '--- application tests diff ---'
git diff --unified=50 "$base" HEAD -- \
  web/src/routes/Dashboard.test.tsx \
  web/src/components/DayStrip.test.tsx \
  web/src/components/StockBar.test.tsx \
  web/src/lib/dashboard.test.ts

Length of output: 70531


🏁 Script executed:

#!/bin/bash
set -euo pipefail
base="$(git merge-base HEAD origin/main 2>/dev/null || git rev-parse HEAD~1)"

printf '%s\n' '--- CSS and guard diff ---'
git diff --unified=100 "$base" HEAD -- \
  web/src/styles.css \
  web/src/styles.grades.test.ts \
  web/src/styles.test.ts \
  web/src/routes/Login.styles.test.ts

printf '%s\n' '--- translation diff ---'
git diff --unified=40 "$base" HEAD -- web/src/i18n/en.ts web/src/i18n/es.ts web/src/i18n/tl.ts

printf '%s\n' '--- selectors and declarations in final CSS ---'
rg -n -A18 -B8 'dash-list|`@container`|daystrip|trend-|stock-(total|ledger)|grade-[1-8]|meter-stack|panel:has' web/src/styles.css

Length of output: 79422


🏁 Script executed:

#!/bin/bash
set -euo pipefail
base="$(git merge-base HEAD origin/main 2>/dev/null || git rev-parse HEAD~1)"

printf '%s\n' '--- screenshot specification diff ---'
git diff --unified=80 "$base" HEAD -- tools/simulation/ui/specs-screenshots/screenshots.spec.ts

printf '%s\n' '--- css token utility outline and relevant implementation ---'
ast-grep outline web/src/test/cssTokens.ts --items all
rg -n -A80 -B10 'export (function|const)|function (resolveTokens|declaredKeys|contrast)|type Mode|interface' web/src/test/cssTokens.ts

printf '%s\n' '--- React namespace usage patterns ---'
rg -n -A1 -B1 'React\.ReactNode' web/src -g '*.tsx'

printf '%s\n' '--- final references to deleted Sparkline symbols and old dashboard translation keys ---'
rg -n -A2 -B2 '\b(Sparkline|sparkline|sparklineLabel|henDayCaption|eggsAvailableMessage)\b' web tools || true

printf '%s\n' '--- dashboard grid panel rules outside changed region ---'
rg -n -A24 -B10 '(^|[,{]\s*)\.panel(\s|,|\{|$)|\.dash-grid' web/src/styles.css

Length of output: 21882


@mforce: I completed the static review.

I found no actionable defects.

The recent-sales layout uses the panel width through the named container query. The DayStrip preserves one slot per report day and marks the seven-day boundary. The grade-token guard checks both theme base blocks and prevents the dark-theme inheritance case. The updated CSS guards also handle @container and no longer classify border-radius as a colour property.

You are interacting with an AI system.

…e guards

From a local adversarial review of the previous commit. Six real defects, and
two of them contradicted that commit's own stated rationale.

A grade's hue was positional, not identity-bearing: `colorIndex` read the index
AFTER the zero-availability rows were filtered out, so the day one grade sold
out, every grade after it changed colour. That is the defect the "not
brand-scoped" argument exists to prevent, and it needed one sale rather than two
deployments. The index now comes from the grade's place in the full row set.

Three hues collided with the colours they sit beside. --grade-6 was dE 4.3 from
--success and --grade-5 dE 11.9 from --warn in light, --grade-4 dE 19.0 from
--error and --grade-1 dE 18.2 from the slate palette's accent in dark — a stock
band reading as a status signal on a screen that shows both. Replaced with a
taupe, a cyan, a crimson and a deeper blue, each holding one hue family across
themes.

The container threshold was set at the wrong end. Just above 26rem the three
intrinsic tracks plus gaps left the customer name 62px in `en` and 45px in `tl`,
which is the collapse the rule beside it claimed to fix. It is 32rem now, the
reference track can yield, and the name track has a 7rem floor. The reference
number also gains the clip the name already had: with nowrap alone it ran under
the amount at a 280px panel.

The strip's rationale was wrong in four files. The line mapped a zero day to the
floor of its viewBox, so a zero was drawn as a drop, not as a plateau. What it
actually failed to do was draw either end of its scale, and it interpolated
between days a daily count has no values between. Corrected to that.

styles.grades.test.ts was vacuous against both breaks it named: a near-duplicate
hex passed `new Set().size`, and a rule repainting every band passed a check
that only asserted the grade rules' TEXT was present. It measures CIE76 distance
now, against the other hues, the semantic colours and every palette's accent,
and rejects any other rule that can paint a band. styles.test.ts let a literal
colour through inside color-mix — this branch introduced the stylesheet's first
one — and had dropped border-image when its colour-property list was narrowed.

Also: the day strip's accessible name now says how many days have nothing
recorded, rather than announcing "lowest 0"; the peak is called the same thing
in the label and on screen, per locale; the stock total no longer reads
"1egg available" in its text content; and DayStripSlot.value is gone, having had
no reader but the test that asserted it.
@mforce

mforce commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner Author

Local adversarial review found six real defects; pushed 38d9ac0

CodeRabbit's round was clean, and it was a static review. The defect class this change lives in is the one only a render catches, so I ran a local adversarial pass as AGENTS.md requires before a first push — late, and it should have run before 9b6f1f0. Two reviewers: one told to refute the logic and the changed guards, one building a render harness across all four farm palettes in both themes with edge data.

Two of the findings contradicted my own stated rationale, which is the part worth reading.

1 — A grade's hue was positional, so one sale recoloured the panel

colorIndex read the index after .filter(r => r.available > 0). Large, Medium, Small gave 1, 2, 3; the day Large sold out, Medium became 1 and Small became 2. Same farm, two days, different colours.

I argued three times in this diff, and again in the PR body, that a grade must not change colour between two captures or before/after screenshots stop being comparable — and then shipped a version that breaks on one sale rather than two deployments. The index now comes from the grade's place in the full row set. The old test, which I had renamed to "indexes the hue by SURVIVING segment", was pinning the defect in place; it is replaced by one that sells a grade out and asserts the others keep their hue.

2 — Three hues collided with the colours they sit beside

Measured, not eyeballed. In light, --grade-6 was dE 4.3 from --success and --grade-5 11.9 from --warn. In dark, --grade-4 was 19.0 from --error, and --grade-1 18.2 from the slate palette's accent — which the default palette never showed. A stock band reading as a status signal, on a screen that renders status badges directly beside it.

Replaced with a taupe, a true cyan, a crimson and a deeper blue, each holding one hue family across both themes. Every grade now clears every semantic colour and all four palette accents.

3 — The threshold was set at the wrong end of the range

The comment beside the container query says a viewport query "was wrong: the name column collapsed to nothing". The 26rem threshold left a live band doing the same thing: at a 418px panel the name measured 62px in en and 45px in tl — about six characters of a customer name — reachable at roughly a 1000px window or a half-screen split.

It is 32rem now, the reference track is minmax(0, max-content) so it can yield, and the name track has a 7rem floor. Measured on the running app at the exact failing width: the customer column is 279px at a 956px viewport, up from 62px.

4 — The reference number had no clip

.ref had white-space: nowrap and nothing else, so a real SO- reference beside a seven-figure total ran under the amount at a 280px panel. It gets the same overflow: hidden; text-overflow: ellipsis the name already had.

5 — My justification for the day strip was false

I wrote, in four files plus the PR body and the issue thread, that the old line "drew a segment straight through days with nothing recorded, so a stretch nobody had entered read as a plateau of real production".

It did not. sparkline mapped a zero day to y = SPARK_H, the floor of the viewBox, so a zero was drawn as a drop — which is what the before screenshot actually shows. What the line failed to do was draw either end of its scale, so nothing said the bottom meant zero rather than the window's own minimum; and it interpolated between days, implying values a daily count does not have. Those are real and they are what the strip fixes. The reason I gave was not one the code supported, and I repeated it six times. Corrected everywhere.

6 — The new guard was vacuous against both breaks it named

styles.grades.test.ts asserted new Set(values).size and the presence of a rule's text. Both were hollow:

  • --grade-8: #2076a1 beside --grade-1: #2076a0 — one hex digit apart, invisible to a human — passed.
  • Appending .meter-stack > span[class] { background: var(--error); } repainted every band one colour, confirmed in Chromium, with the file green.

It measures CIE76 distance now — against the other hues, the semantic colours, and every palette's accent — and rejects any rule other than the eight that can paint a band. deltaE lives in test/cssTokens.ts beside contrast, which answers a different question: contrast is "can this be seen against that", this is "can these two be told apart".

Two holes in styles.test.ts as well. Its colour check accepted color-mix(in oklab, #ff0000 7%, var(--surface)) because the value contains a token — and this branch introduced the stylesheet's first color-mix, so the hole went from inert to live. And narrowing the border regex had dropped border-image, the one other border longhand that carries a colour.

Mutations

Every one of these goes red now, re-run after the fix:

mutation before after
a rule repainting every band one colour green red
--grade-8 one hex digit from --grade-1 green red
the old --grade-6 (dE 4.3 from --success) green red
the old dark --grade-1 (dE 18.2 from slate's accent) green red
color-mix with a literal instead of a token green red
border-image carrying literal colours green red
drop --grade-8 from the dark base red red

Smaller

The strip's accessible name now says how many days have nothing recorded, instead of announcing "lowest 0" — the conflation the redraw removed for sighted users. The peak is called the same thing in the label and on screen, in each locale. The stock total no longer reads 1egg available in its text content. DayStripSlot.value is gone; its only reader was the test asserting it.

Verification

2941 tests green, typecheck and build clean, and the app re-driven in a real browser at the widths the review named. Screenshots below are from 38d9ac0.

Dashboard, dark
Dashboard, light
Recent sales at the width that used to collapse the name column

@mforce

mforce commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

Pushed 38d9ac0, which fixes six defects a local adversarial pass found in 9b6f1f0 — the commit your last round read. Rule of thumb I follow: a finding against a previous fix is the most likely to be real, so this commit is the one worth the scrutiny, not the original.

Two contradicted the diff's own stated rationale. stockBar assigned the grade hue from the index AFTER filtering out zero-availability rows, so one grade selling out recoloured every grade after it — the exact instability the "hues are not brand-scoped" argument exists to prevent. And styles.grades.test.ts was vacuous against both breaks it named: a one-hex-digit near-duplicate passed new Set().size, and a rule repainting every band passed a check that only asserted the grade rules' text was present.

Worth a close look at: the CIE76 deltaE helper added to web/src/test/cssTokens.ts and the thresholds it is used at; the cascade check in styles.grades.test.ts, whose scope limit I stated in a comment rather than pretending it is total; the container threshold moved 26rem to 32rem with a 7rem floor on the name track; and the two styles.test.ts holes (a literal colour inside color-mix, and border-image dropped when the colour-property regex was narrowed).

@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

base="$(git merge-base HEAD origin/main 2>/dev/null || git rev-parse HEAD~1)"
printf '%s\n' '--- commits since base ---'
git log --oneline "$base"..HEAD
printf '%s\n' '--- 38d9ac0 diff: affected files ---'
git diff-tree --no-commit-id --name-status -r 38d9ac0
printf '%s\n' '--- focused diff ---'
git diff --unified=100 38d9ac0^ 38d9ac0 -- \
  web/src/components/StockBar.tsx \
  web/src/components/StockBar.test.tsx \
  web/src/test/cssTokens.ts \
  web/src/styles.grades.test.ts \
  web/src/styles.test.ts \
  web/src/styles.css \
  web/src/routes/Dashboard.tsx \
  web/src/components/DayStrip.tsx \
  web/src/components/DayStrip.test.tsx \
  web/src/i18n/en.ts \
  web/src/i18n/es.ts \
  web/src/i18n/tl.ts
printf '%s\n' '--- outlines ---'
ast-grep outline web/src/components/StockBar.tsx --items all
ast-grep outline web/src/test/cssTokens.ts --items all
ast-grep outline web/src/styles.grades.test.ts --items all
ast-grep outline web/src/styles.test.ts --items all
printf '%s\n' '--- relevant final CSS ---'
rg -n -A50 -B15 'panel:has|dash-list|`@container`|\.ref\b|customer|meter-stack|grade-[0-9]|color-mix|border-image' web/src/styles.css
printf '%s\n' '--- final grade guard ---'
cat -n web/src/styles.grades.test.ts
printf '%s\n' '--- final css token helper relevant sections ---'
rg -n -A100 -B10 'function (contrast|deltaE|resolve|parse)|export (function|const)|type |interface ' web/src/test/cssTokens.ts
printf '%s\n' '--- final generic styles guard relevant sections ---'
rg -n -A100 -B10 'color|border|color-mix|token|literal' web/src/styles.test.ts

Length of output: 151581


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- test files and CSS references ---'
fd -t f '(StockBar|Dashboard|DayStrip|styles\.grades|styles)\.(test\.)?(ts|tsx)$' web/src | sort
printf '%s\n' '--- StockBar final implementation and tests ---'
cat -n web/src/components/StockBar.tsx
cat -n web/src/components/StockBar.test.tsx
printf '%s\n' '--- DayStrip final implementation and tests ---'
cat -n web/src/components/DayStrip.tsx
cat -n web/src/components/DayStrip.test.tsx
printf '%s\n' '--- Dashboard final relevant markup ---'
rg -n -A170 -B20 'dash-list|DayStrip|StockBar|recent|sale|ref' web/src/routes/Dashboard.tsx
printf '%s\n' '--- CSS token consumer usages ---'
rg -n -A4 -B4 '\b(deltaE|contrast|resolveTokens|declaredKeys)\b' web/src -g '*.ts' -g '*.tsx'
printf '%s\n' '--- palette and semantic declarations ---'
rg -n -A35 -B6 'data-palette|--(success|warn|error|accent|grade-[0-9])' web/src/styles.css

Length of output: 70118


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- dashboard model implementation and tests ---'
cat -n web/src/lib/dashboard.ts
cat -n web/src/lib/dashboard.test.ts
printf '%s\n' '--- commit-specific Dashboard tests ---'
git diff --unified=80 38d9ac0^ 38d9ac0 -- web/src/routes/Dashboard.test.tsx web/src/lib/dashboard.ts web/src/lib/dashboard.test.ts
printf '%s\n' '--- all dashboard colour-valued CSS declarations, with selector context ---'
python3 - <<'PY'
from pathlib import Path
import re
css = Path("web/src/styles.css").read_text()
for m in re.finditer(r'([^{}]+)\{([^{}]*)\}', css, re.S):
    selector, body = m.group(1).strip(), m.group(2)
    if re.search(r'(?m)^\s*(?:[\w-]*color|background(?:-color|-image)?|border(?:-(?:top|right|bottom|left|block|inline|image))?|stroke|fill|outline(?:-color)?)\s*:', body):
        if any(x in selector for x in ('.capture-', '.trend', '.day', '.stock-', '.meter-stack', '.dash-list', '.panel-wide')):
            print(selector.replace('\n', ' '))
            for d in body.split(';'):
                if re.match(r'\s*(?:[\w-]*color|background(?:-color|-image)?|border(?:-(?:top|right|bottom|left|block|inline|image))?|stroke|fill|outline(?:-color)?)\s*:', d):
                    print("  "+d.strip())
PY
printf '%s\n' '--- all active CSS selector forms that can style a meter-stack descendant ---'
rg -n -A3 -B2 '\.meter-stack|\.panel\s+span|span\[class\]|span\.' web/src/styles.css
printf '%s\n' '--- current branch status ---'
git status --short

Length of output: 65636


@mforce: I completed the focused review of 38d9ac0.

I found two follow-up items.

  1. P2 — Extend the literal-colour guard.
    web/src/styles.test.ts still accepts literal CSS colours that its regex does not list when the value also contains var(--...).

    For example, this passes:

    .day {
      background: color-mix(in oklab, red 7%, var(--surface));
    }

    The guard rejects hex, rgb(), hsl(), oklch(), and lab(). It does not reject named colours, hwb(), lch(), oklab(), or color(). This leaves the new color-mix protection incomplete.

  2. P3 — Correct stale DayStrip rationale comments.
    web/src/components/DayStrip.tsx and web/src/lib/dashboard.test.ts still state that the replaced line chart rendered missing days as a plateau. The corrected rationale in web/src/lib/dashboard.ts and web/src/styles.css says that zero values rendered as a drop to the viewBox floor. Update these comments to keep the code documentation consistent.

The grade-index fix is correct. The CIE76 checks cover grade pairs, semantic colours, palette accents, and both meter surfaces. The direct .meter-stack cascade check also catches the reported repaint rule within its documented scope.


✏️ Learnings added
Learnt from: mforce
URL: https://github.com/mforce/cluckwork/pull/781

Timestamp: 2026-09-12T08:42:43.958Z
Learning: For Dashboard stock-bar colour guards, `web/src/styles.grades.test.ts` must protect both perceptual distinction and CSS cascade behavior. Grade hues must remain stable when a different grade reaches zero availability, must remain distinct from semantic colours and all farm palette accents, and CSS selectors that directly target `.meter-stack` descendants must not override the grade backgrounds.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

Both findings from the second review round, both against the previous fix.

The literal-colour check added in 38d9ac0 enumerated hex and four colour
functions, which is the losing game the surrounding comment already warned
about: `color-mix(in oklab, red 7%, var(--surface))` walked straight through it,
and so did hwb(), lch(), oklab() and color(). It is inverted now —
`literalColourIn` strips the var() references and inspects whatever is left,
against the full CSS named-colour set and every colour function. Its limit is
stated in the helper rather than implied: both lists are finite and CSS Color
keeps growing, which is why the caller still requires a token to be present as
well. Seven literal syntaxes were mutated through the live color-mix and all
seven go red.

The corrected day-strip rationale had landed in two files and not the other
three. DayStrip.tsx, lib/dashboard.test.ts and routes/Dashboard.test.tsx still
said the line rendered unrecorded days as a plateau; the review named two of
them and there were three.
@mforce

mforce commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Both findings were real and are fixed in 51bb668.

P2 — the literal-colour guard. Confirmed: color-mix(in oklab, red 7%, var(--surface)) passed. Rather than extend the list to six syntaxes, I inverted it, because enumerating colour syntaxes is the losing game the comment right above that check already warns about — and I had just lost it. literalColourIn in test/cssTokens.ts strips the var() references and inspects what is left, against the full CSS named-colour set and every colour-producing function. Its limit is written into the helper instead of implied: both lists are finite and CSS Color keeps growing, so the caller still requires a token to be present as well; this narrows what may sit beside the token, it does not stand alone.

Mutated through the live color-mix, all seven go red, and the real value still passes:

literal before after
red green red
rebeccapurple green red
hwb(0 0% 0%) green red
lch(50% 40 30) green red
oklab(0.5 0.1 0.1) green red
color(display-p3 1 0 0) green red
#ff0000 red red

P3 — the stale rationale. Confirmed, and it was worse than reported: three files still carried it, not two. grep -rn plateau finds DayStrip.tsx, lib/dashboard.test.ts and also routes/Dashboard.test.tsx, which the round did not name. All three now say what the line actually did — a zero landed on the floor of the viewBox and that floor was never drawn — rather than the plateau claim, which was false. I had said in the previous comment that I corrected this in four files; I corrected two.

2941 tests green, typecheck and build clean.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- commit and changed files ---'
git show --no-ext-diff --format=fuller --name-status 51bb668

printf '%s\n' '--- focused diff ---'
git diff --unified=100 51bb668^ 51bb668 -- \
  web/src/test/cssTokens.ts \
  web/src/styles.test.ts \
  web/src/components/DayStrip.tsx \
  web/src/lib/dashboard.test.ts \
  web/src/routes/Dashboard.test.tsx

printf '%s\n' '--- outlines ---'
ast-grep outline web/src/test/cssTokens.ts --items all
ast-grep outline web/src/styles.test.ts --items all
ast-grep outline web/src/components/DayStrip.tsx --items all
ast-grep outline web/src/lib/dashboard.test.ts --items all
ast-grep outline web/src/routes/Dashboard.test.tsx --items all

printf '%s\n' '--- literal colour helper, guard, and test coverage ---'
rg -n -A100 -B12 '\b(literalColourIn|named|colour|color|var)\b' \
  web/src/test/cssTokens.ts web/src/styles.test.ts

printf '%s\n' '--- remaining stale plateau wording ---'
rg -n -i -A2 -B2 '\bplateau\b|straight through days|nothing recorded' web/src tools || true

printf '%s\n' '--- literal colour function / color-mix stylesheet uses ---'
rg -n -A3 -B3 'color-mix|(?:#(?:[0-9a-fA-F]{3,8})\b|(?:rgb|hsl|hwb|lab|lch|oklab|oklch|color)\()' web/src/styles.css web/src -g '*.css' -g '*.test.ts' || true

Length of output: 178285


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- exact commit diff ---'
git diff --unified=20 51bb668^ 51bb668 -- web/src/test/cssTokens.ts web/src/styles.test.ts web/src/components/DayStrip.tsx web/src/lib/dashboard.test.ts web/src/routes/Dashboard.test.tsx

printf '%s\n' '--- cssTokens helper ---'
rg -n -A80 -B10 'export function literalColourIn|const CSS_NAMED|COLOUR_FUNCTION|literalColourIn' web/src/test/cssTokens.ts

printf '%s\n' '--- dashboard guard loop ---'
sed -n '235,285p' web/src/styles.test.ts

printf '%s\n' '--- direct helper tests and imports ---'
rg -n -A30 -B8 'literalColourIn\(' web/src -g '*.test.ts' -g '*.ts'

printf '%s\n' '--- corrected rationale comments ---'
rg -n -A8 -B3 'viewBox|plateau|zero day|zero value' \
  web/src/components/DayStrip.tsx \
  web/src/lib/dashboard.test.ts \
  web/src/routes/Dashboard.test.tsx \
  web/src/lib/dashboard.ts \
  web/src/styles.css

printf '%s\n' '--- current dashboard color declarations and selector blocks ---'
sed -n '1804,1870p' web/src/styles.css

Length of output: 35768


@mforce I found one follow-up item in 51bb668.

  1. P2 — Inspect literal colours in var() fallbacks.
    literalColourIn removes the full var() expression, including its fallback, in web/src/test/cssTokens.ts Line 232.

    This declaration passes the dashboard-surface guard:

    .day {
      background: var(--day-fill, red);
    }

    It contains var(--, so tokenised is true. The helper then replaces var(--day-fill, red) with whitespace. It does not detect red.

    The fallback is an active colour when --day-fill is undefined. Preserve and inspect fallback content, or add a separate check for literal colours in var() fallback arguments.

The DayStrip rationale corrections are consistent across all three files. The color-mix tests now cover the reported literal syntaxes, but this fallback path remains unguarded.


✏️ Learnings added
Learnt from: mforce
URL: https://github.com/mforce/cluckwork/pull/781

Timestamp: 2026-09-12T08:46:36.015Z
Learning: For the Dashboard literal-colour guard in `web/src/styles.test.ts`, a CSS custom-property fallback such as `var(--token, red)` can supply an active literal colour. Any `literalColourIn` helper in `web/src/test/cssTokens.ts` must inspect fallback arguments rather than remove the entire `var()` expression.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: mforce
URL: https://github.com/mforce/cluckwork/pull/781

Timestamp: 2026-09-12T08:42:44.058Z
Learning: For Dashboard stock-bar colour guards, `web/src/styles.grades.test.ts` must protect both perceptual distinction and CSS cascade behavior. Grade hues must remain stable when a different grade reaches zero availability, must remain distinct from semantic colours and all farm palette accents, and CSS selectors that directly target `.meter-stack` descendants must not override the grade backgrounds.

You are interacting with an AI system.

Third review round, again against the previous fix. `literalColourIn` stripped
the whole `var()` expression including its fallback, so `var(--day-fill, red)`
left nothing to inspect and passed: the value contains `var(--`, and the
literal it would actually render was deleted before the check ran. A fallback
renders whenever the token is undefined, so it is a real colour.

The fallback is now unwrapped rather than deleted, and the loop repeats, so
`var(--a, var(--b, red))` unwraps all the way down. Three fallback forms go
red and a token-only nested fallback still passes.
@mforce

mforce commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Real, and fixed in 0a01127. Reproduced first: stripping var(--day-fill, red) left " ", so there was nothing for the check to look at and the literal it would actually render had been deleted before the check ran.

A fallback renders whenever the token is undefined, so it is a real colour. It is unwrapped now rather than deleted, and the loop repeats so var(--a, var(--b, red)) unwraps all the way down.

value under .day { background: … } before after
var(--day-fill, red) green red
var(--a, var(--b, #ff0000)) green red
var(--a, hwb(0 0% 0%)) green red
var(--a, var(--surface)) — token-only, must pass green green

2941 tests green, typecheck and build clean.

Worth noting where this loop now stands, so the record is honest: rounds two and three have both been findings in the guard, not in the product — a control for a control. My own rule is to stop triggering at two such rounds and let the owner decide, and I raised exactly that. The owner's answer was to keep going until you have nothing left worth fixing, so I am continuing on their explicit instruction rather than by default.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- commit and affected files ---'
git show --no-ext-diff --format=fuller --name-status 0a01127

printf '%s\n' '--- focused diff ---'
git diff --unified=100 0a01127^ 0a01127 -- \
  web/src/test/cssTokens.ts \
  web/src/styles.test.ts

printf '%s\n' '--- outlines ---'
ast-grep outline web/src/test/cssTokens.ts --items all
ast-grep outline web/src/styles.test.ts --items all

printf '%s\n' '--- final literal-colour helper and its callers ---'
rg -n -A120 -B15 '\b(literalColourIn|CSS_NAMED|COLOUR_FUNCTION|var\()\b' \
  web/src/test/cssTokens.ts web/src/styles.test.ts

printf '%s\n' '--- all helper uses and related tests ---'
rg -n -A20 -B10 '\bliteralColourIn\s*\(' web/src -g '*.ts' -g '*.tsx'

printf '%s\n' '--- current dashboard colour declarations ---'
rg -n -A5 -B5 'color-mix|var\(--[^)]*,|(?:background|color|border(?:-image|-color)?|fill|stroke)\s*:' \
  web/src/styles.css

printf '%s\n' '--- stale rationale wording ---'
rg -n -i -A2 -B2 '\bplateau\b|straight through days|nothing recorded' web/src tools || true

printf '%s\n' '--- current branch status ---'
git status --short

Length of output: 90739


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- literal-colour guard test block ---'
sed -n '220,290p' web/src/styles.test.ts

printf '%s\n' '--- test support that mutates or exercises stylesheet guards ---'
rg -n -A45 -B12 '(readFileSync|writeFileSync|replace\(|color-mix|fallback|var\(--a|day-fill|literal colour|literalColour)' \
  web/src/styles.test.ts web/src/test -g '*.test.ts' -g '*.ts'

printf '%s\n' '--- exact helper implementation and commit history for this line range ---'
git blame -L 218,251 0a01127 -- web/src/test/cssTokens.ts
git diff --check 0a01127^ 0a01127

Length of output: 51001


@mforce I confirmed that 0a01127 fixes the reported fallback defect. The repeated unwrapping detects nested fallback values while token-only values remain valid.

I found one follow-up item.

  1. P2 — Add permanent tests for literalColourIn fallback handling.
    web/src/styles.test.ts only scans the current stylesheet. No dashboard rule currently contains a var() fallback. Therefore, the suite does not execute the new fallback branch in web/src/test/cssTokens.ts.

    Add direct tests for literalColourIn that cover:

    • var(--day-fill, red)
    • var(--a, var(--b, #ff0000))
    • var(--a, hwb(0 0% 0%))
    • var(--a, var(--surface))

    This makes the reproduced mutations permanent regression coverage instead of a manual verification step.


🧠 Learnings used
Learnt from: mforce
URL: https://github.com/mforce/cluckwork/pull/781

Timestamp: 2026-09-12T08:46:36.094Z
Learning: For the Dashboard literal-colour guard in `web/src/styles.test.ts`, a CSS custom-property fallback such as `var(--token, red)` can supply an active literal colour. Any `literalColourIn` helper in `web/src/test/cssTokens.ts` must inspect fallback arguments rather than remove the entire `var()` expression.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

You are interacting with an AI system.

…g needed

Fourth review round. The stylesheet scan is a SAMPLE: it exercises only the
branches the current CSS happens to reach, and no rule uses a var() fallback,
so the fallback branch added in 0a01127 had no permanent test and was reached
only by a throwaway mutation. Twenty cases now call literalColourIn the way the
scan does and assert the literal it should name, fallbacks and nesting included.

Writing them turned up something the round did not ask for. A mutation removing
the repeat loop from literalColourIn left every test green, so the loop could
not be shown to matter; comparing looped against single-pass over 42 nested
shapes found no value where the two reach different verdicts. `[^()]*` cannot
cross a paren, so the first replace matches the innermost var(--x, <literal>)
and leaves that literal bare in the same pass, and later passes only tidy
wrapper text that carries no colour. The loop is gone and the reason is in the
helper, because complexity costs double in the thing you are trusting.
@mforce

mforce commented Sep 12, 2026 •

Copy link
Copy Markdown
Owner Author

Fair, and fixed in 2e8bb30. The stylesheet scan is a sample — it reaches only the branches the current CSS happens to use — so the fallback branch had no permanent test and was only ever hit by a throwaway mutation. Twenty cases now call literalColourIn the way the scan does and assert the literal it should name, covering all four values you listed plus hex, named, five colour functions, and the token-only shapes that must pass.

Writing them turned up something you did not ask about, and it went the other way. A mutation removing the repeat loop from literalColourIn left every test green — so the loop could not be shown to matter. I compared looped against single-pass across 42 nested shapes and found no value where the two reach different verdicts: [^()]* cannot cross a paren, so the first replace matches the innermost var(--x, <literal>) and leaves that literal bare in the same pass, and every later pass only tidies wrapper text that carries no colour.

Correction. This said the loop was gone. It was not: 2e8bb30 touched web/src/styles.test.ts and nothing else. The edit lived in a shell step whose leading cd web failed, && short-circuited, and the Python never ran — and I read the green suite that followed as confirming a change that had not happened. Caught in the next round; actually removed in 9c42d92, which git show --stat confirms.

So the loop should be gone rather than kept as an untestable branch, and the reasoning belongs in the helper. A guard is the thing you are trusting, so complexity in it costs double.

The two mutations that matter still go red against the simplified helper: deleting the fallback unwrap fails 3 cases, dropping the named-colour list fails 4.

2961 tests green, typecheck and build clean.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- commit and changed files ---'
git show --no-ext-diff --format=fuller --name-status 2e8bb30

printf '%s\n' '--- focused diff ---'
git diff --unified=100 2e8bb30^ 2e8bb30 -- \
  web/src/test/cssTokens.ts \
  web/src/test/cssTokens.test.ts \
  web/src/styles.test.ts

printf '%s\n' '--- outlines ---'
for file in web/src/test/cssTokens.ts web/src/test/cssTokens.test.ts web/src/styles.test.ts; do
  if [ -f "$file" ]; then
    ast-grep outline "$file" --items all
  fi
done

printf '%s\n' '--- literalColourIn implementation and tests ---'
rg -n -A120 -B15 '\b(literalColourIn|CSS_NAMED|COLOUR_FUNCTION|var\()\b' \
  web/src/test/cssTokens.ts web/src/test/cssTokens.test.ts web/src/styles.test.ts 2>/dev/null || true

printf '%s\n' '--- all literalColourIn call sites ---'
rg -n -A10 -B8 '\bliteralColourIn\s*\(' web/src -g '*.ts' -g '*.tsx'

printf '%s\n' '--- current dashboard colour declaration contexts ---'
rg -n -A6 -B6 'color-mix|var\(--[^)]*,|(?:background|color|border(?:-image|-color)?|fill|stroke)\s*:' \
  web/src/styles.css

printf '%s\n' '--- stale rationale wording ---'
rg -n -i -A2 -B2 '\bplateau\b|straight through days|nothing recorded' web/src tools || true

printf '%s\n' '--- diff whitespace and status ---'
git diff --check 2e8bb30^ 2e8bb30
git status --short

Length of output: 106947


@mforce I confirmed that the direct literalColourIn cases add permanent coverage for the fallback paths. They include the four requested cases and the token-only cases.

I found one follow-up item.

  1. P3 — Remove the loop that the commit says it removed.
    web/src/test/cssTokens.ts Line 235 through Line 241 still contains the do...while loop and its loop-specific comment. Commit 2e8bb30 changes only web/src/styles.test.ts.

    The new test for var(--a, var(--b, red)) supports the single-pass reasoning. The fallback replacement exposes red from the inner var() call, and literalColourIn detects it without another pass.

    Remove the loop and update the comment. This will make the implementation match the stated rationale and remove unneeded guard complexity.


🧠 Learnings used
Learnt from: mforce
URL: https://github.com/mforce/cluckwork/pull/781

Timestamp: 2026-09-12T08:46:36.094Z
Learning: For the Dashboard literal-colour guard in `web/src/styles.test.ts`, a CSS custom-property fallback such as `var(--token, red)` can supply an active literal colour. Any `literalColourIn` helper in `web/src/test/cssTokens.ts` must inspect fallback arguments rather than remove the entire `var()` expression.

You are interacting with an AI system.

2e8bb30's message describes removing the repeat loop from literalColourIn. It
did not: that commit touched web/src/styles.test.ts and nothing else. The edit
was in a shell step whose leading `cd web` failed, so `&&` short-circuited and
the Python never ran, and the green suite that followed was read as confirming
a change that had not happened. Verifying the outcome rather than the diff is
what let a commit message state something untrue.

The loop is gone now, `git show --stat` says so, and the two mutations that
matter still bite: deleting the fallback unwrap fails 3 cases, dropping the
named-colour list fails 4.
@mforce

mforce commented Sep 12, 2026

Copy link
Copy Markdown
Owner Author

Confirmed, and this is the best catch of the run. 2e8bb30 touched web/src/styles.test.ts and nothing else — git show --stat says so.

The cause is worth writing down. The edit lived in a shell step beginning cd web && python3 …; the working directory was already web, so cd failed, && short-circuited, and the Python never ran. The suite I ran immediately afterwards passed — because nothing had changed — and I read that green as confirming the edit. I verified the outcome instead of the diff, which is how a commit message came to state something untrue. That is worse than the loop, because the loop was harmless and the record was not.

Removed for real in 9c42d92, verified by git show --stat rather than by a passing suite. The two mutations that matter still bite against the simplified helper: deleting the fallback unwrap fails 3 cases, dropping the named-colour list fails 4. I have also corrected the earlier comment that claimed the loop was gone, rather than leaving it standing.

2961 tests green, typecheck and build clean.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- commits under review ---'
git log --oneline --decorate -12

printf '%s\n' '--- 9c42d92 commit and diff ---'
git show --no-ext-diff --format=fuller --name-status 9c42d92
git diff --check 9c42d92^ 9c42d92
git diff --unified=100 9c42d92^ 9c42d92 -- \
  web/src/test/cssTokens.ts \
  web/src/test/cssTokens.test.ts \
  web/src/styles.test.ts

printf '%s\n' '--- helper and tests ---'
ast-grep outline web/src/test/cssTokens.ts --items all
ast-grep outline web/src/test/cssTokens.test.ts --items all
rg -n -A130 -B15 '\b(literalColourIn|CSS_NAMED|COLOUR_FUNCTION|var\()\b' \
  web/src/test/cssTokens.ts web/src/test/cssTokens.test.ts web/src/styles.test.ts

printf '%s\n' '--- current literal-colour guard call sites ---'
rg -n -A12 -B8 '\bliteralColourIn\s*\(' web/src -g '*.ts' -g '*.tsx'

printf '%s\n' '--- current CSS use of var fallbacks and colour contexts ---'
rg -n -A5 -B5 'var\(--[^)]*,|color-mix|(?:background(?:-color|-image)?|color|border(?:-image|-color)?|fill|stroke|outline(?:-color)?)\s*:' \
  web/src/styles.css

printf '%s\n' '--- status ---'
git status --short

Length of output: 36298


@mforce I reviewed 9c42d92.

I found no actionable defects.

literalColourIn now uses one replacement pass. The regex reaches the innermost var() fallback and exposes its literal value in that pass. The direct cases in web/src/styles.test.ts cover token-only values, literals, and nested var() fallbacks.

The shell inspection stopped after a reference to nonexistent web/src/test/cssTokens.test.ts. The available output still included the complete 9c42d92 diff, the helper implementation, and the direct test cases.


🧠 Learnings used
Learnt from: mforce
URL: https://github.com/mforce/cluckwork/pull/781

Timestamp: 2026-09-12T08:46:36.094Z
Learning: For the Dashboard literal-colour guard in `web/src/styles.test.ts`, a CSS custom-property fallback such as `var(--token, red)` can supply an active literal colour. Any `literalColourIn` helper in `web/src/test/cssTokens.ts` must inspect fallback arguments rather than remove the entire `var()` expression.

You are interacting with an AI system.

@coderabbitai

coderabbitai Bot commented Sep 12, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- commit metadata and changed files ---'
git show --no-ext-diff --format=fuller --name-status 9c42d92

printf '%s\n' '--- focused diff ---'
git diff --unified=100 9c42d92^ 9c42d92 -- \
  web/src/test/cssTokens.ts \
  web/src/test/cssTokens.test.ts \
  web/src/styles.test.ts

printf '%s\n' '--- relevant outlines ---'
ast-grep outline web/src/test/cssTokens.ts --items all
ast-grep outline web/src/test/cssTokens.test.ts --items all
ast-grep outline web/src/styles.test.ts --items all

printf '%s\n' '--- final helper and direct tests ---'
rg -n -A130 -B15 '\b(literalColourIn|CSS_NAMED|COLOUR_FUNCTION|var\()\b' \
  web/src/test/cssTokens.ts web/src/test/cssTokens.test.ts web/src/styles.test.ts

printf '%s\n' '--- all helper callers ---'
rg -n -A12 -B10 '\bliteralColourIn\s*\(' web/src -g '*.ts' -g '*.tsx'

printf '%s\n' '--- inspect comments and diff integrity ---'
git diff --check 9c42d92^ 9c42d92
git status --short

printf '%s\n' '--- active dashboard colour declarations ---'
rg -n -A5 -B5 'color-mix|var\(--[^)]*,|(?:background|color|border(?:-image|-color)?|fill|stroke)\s*:' \
  web/src/styles.css

Length of output: 36039


@mforce I completed the focused review of 9c42d92.

I found no actionable defect.

literalColourIn now matches the stated single-pass behavior. For nested var() expressions, [^()]* reaches the innermost fallback. That replacement exposes the literal colour in the same pass. The direct cases in web/src/styles.test.ts cover this behavior and token-only values.

I used the existing fallback-handling guidance during this review. I did not run the test suite, typecheck, or build in the shell environment.

🐇 ✅


🧠 Learnings used
Learnt from: mforce
URL: https://github.com/mforce/cluckwork/pull/781

Timestamp: 2026-09-12T08:46:36.094Z
Learning: For the Dashboard literal-colour guard in `web/src/styles.test.ts`, a CSS custom-property fallback such as `var(--token, red)` can supply an active literal colour. Any `literalColourIn` helper in `web/src/test/cssTokens.ts` must inspect fallback arguments rather than remove the entire `var()` expression.

Learnt from: mforce
URL: https://github.com/mforce/cluckwork/pull/781

Timestamp: 2026-09-12T08:42:44.058Z
Learning: For Dashboard stock-bar colour guards, `web/src/styles.grades.test.ts` must protect both perceptual distinction and CSS cascade behavior. Grade hues must remain stable when a different grade reaches zero availability, must remain distinct from semantic colours and all farm palette accents, and CSS selectors that directly target `.meter-stack` descendants must not override the grade backgrounds.

You are interacting with an AI system.

@mforce
mforce merged commit 7193ebe into main Sep 12, 2026
11 checks passed
@mforce
mforce deleted the fix/777-dashboard-panels branch September 12, 2026 16:41
mforce pushed a commit that referenced this pull request Sep 12, 2026
🤖 I have created a release *beep* *boop*
---


## [0.1.1](v0.1.0...v0.1.1)
(2026-09-12)


### Bug fixes

* **dashboard:** give recent sales real columns and make both charts
readable ([#781](#781))
([7193ebe](7193ebe))
* **dashboard:** tell a day with no entry from a day that laid no eggs
([#791](#791))
([48c10e5](48c10e5))


### Documentation

* **agents:** walk into package registrations when re-deriving
[#271](#271)
([#790](#790))
([5fab974](5fab974))
* **mcp:** record the MCP server design and what the spike got wrong
([#785](#785))
([3fe5a7f](3fe5a7f)),
closes [#770](#770)

---
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>
mforce added a commit that referenced this pull request Sep 13, 2026
#816 reported the dashboard overflowing at 390 (443/390, a 13-character
amount pushed off screen). The measurement was real and the code was not: the
sim stack had been up 28 hours, so it served an image built before 7193ebe
(#781, 2026-09-12) — the commit that gave that list its container-query narrow
layout, and an ancestor of this branch's own base, seven commits back.

Rebuilt, with the same $9,999,999.99 rows still in the fixture:

  documentElement.scrollWidth / clientWidth   390 / 390
  ul.dash-list grid-template-columns          179.219px 112px
  widest .num right edge                      346.6px   (panel is 353.2px)

So `/` is walked rather than excluded, and the dashboard — the screen the
phone context is FOR — is now covered. The issue is closed as not-a-defect
with the evidence on it.

The rule that would have prevented the whole detour is already in AGENTS.md
and is quoted in this PR's own description: a long-running sim stack serves
the bytes it was built from, not the branch under review. Rebuild before
believing a rendered measurement, including one used only to justify an
exclusion. The spec comment and the decision record both carry that, because
the next person to measure a stale stack will find it convincing too.

Verified: five phone mutants all KILLED with chromium green under each;
full suite 46 passed, 1 skipped across both projects.
mforce added a commit that referenced this pull request Sep 13, 2026
Closes #814

## The gap

`playwright.config.ts` declared one project. It spread `devices["Desktop
Chrome"]`, which carries a 1280x720 viewport, so every spec on every
pull request since 2026-08-08 has only ever rendered the app in a
desktop window — while the product's primary context is a phone in a hen
house, which is the stated reason #674 chose direction B.

This adds the second width. It is the **gate**, not the layout remedy —
#740 stays with the #674 revamp.

## What changed

Two Playwright projects partitioned by a structured `@phone` tag, so
every test runs in exactly one and none runs twice. `npm test` runs
both; no workflow logic changed.

**The two projects differ in the viewport and nothing else**,
deliberately. A spec red in one and green in the other is then
attributable to width alone, which is the entire evidentiary value of
the split. `devices["Pixel 7"]`, `isMobile` and `hasTouch` were measured
against the live stack and rejected: both emulation modes reported
identical `scrollWidth`/`clientWidth`/`innerWidth` on all six walked
routes and an identical tab-bar rect, so they buy nothing here while
adding touch dispatch and a mobile UA. The cost is stated rather than
implied in the config — `isMobile` drives Chromium's mobile
layout-viewport sizing, so a #441-class regression on a real device is
**not** covered, and reopening that means re-measuring, not flipping the
flag.

`signIn` moved off `getByRole("complementary")` to `main#main-content`.
The sidebar is `display: none` below 900px, so the old assertion could
never complete at phone width. The replacement is structural rather than
labelled — the reason the old comment gives still stands, that a persona
left in es/tl by the i18n spec must still be able to sign in — and it is
honestly **narrower**: it proves the authenticated shell mounted, no
longer that any nav chrome rendered.

**The `nav` fixture now throws under the phone layout, and that refusal
is the load-bearing part of this PR.** `worker`, `readonly` and
`session-races` each prove a role gate with
`expect(nav.link(k)).toBeHidden()`. Below 900px those destinations live
inside a *closed* dialog, so every one of those assertions would pass
while the gate stood wide open. Making the fixture unavailable turns
that from something a reviewer has to notice into a construction error.
For the same reason a `MoreSheet` is obtainable only from `openMore()`,
so a sheet-link locator always refers to an open sheet.

## Proving the gate can fail

Three mutants, each killed in the phone project while the **whole**
desktop suite stayed green under it, checked by a new
`MUST_STAY_GREEN_ON` table rather than asserted in a comment. Killing a
phone spec at 390 proves the spec noticed something; it does not prove
the something was width-specific.

| mutant | dies on | desktop under it |
|---|---|---|
| `phone-action-bar-under-tabbar` | `the daily-entry action bar overlaps
the tab bar` | 41 passed, 1 skipped |
| `phone-tabbar-removed` | `there is no tab bar at phone width` | 41
passed, 1 skipped |
| `phone-table-overflow-unclipped` | `/sales`, `/customers`, `/flocks`,
`/history` each `scrolls sideways at phone width` | 41 passed, 1 skipped
|

Each died on exactly the assertion it declares in `EXPECT_MSG_FOR`, and
every one of those lines was copied from an observed run. The overflow
mutant leaves `/daily-entry` and `/stock` at exactly 390 — neither
renders a wide data table — which is why that walk asserts per route and
softly: a hard assertion would stop at `/sales` and report one screen of
four.

`EXPECT_MSG_FOR[nav-role-gate-bypassed]` also had to change. That known
false kill dies inside `signIn`, so its declared line moved from
`getByRole('complementary')` to `locator('main#main-content')` —
re-observed, not translated by hand. Leaving it would have turned a
documented false kill into an unexplained WRONG ASSERTION.

### The instrument is not the obvious one

Two independent designs both proposed injecting a `<style>` element.
**It does nothing here.** The app ships `Content-Security-Policy:
style-src 'self'` with no `'unsafe-inline'` and no nonce, so a
script-created `<style>` parses to nothing — measured, `sheet === null`
with the element sitting in `<head>`. `page.addStyleTag` builds exactly
that element. This is #501's warning wearing a different hat: a CSS
mutant that never installed reports as a *surviving* mutant, accusing
the spec of being vacuous when the mutant is what failed to run.

`page.route` on the stylesheet fails differently — a service worker
serves the original from cache on every load after the first. What works
is `insertRule` on the sheet the page already loaded: CSP governs
loading a style resource, not editing an already-allowed same-origin
one. All three facts are now a third boundary in `src/mutants.ts`,
beside the network and DOM ones.

## Measured cost

Local, against the sim stack, one worker:

| | tests | wall |
|---|---|---|
| `chromium` before | 42 (41 passed, 1 skipped) | 90.6 s |
| `chromium` after | 42 (41 passed, 1 skipped) | 91.0 s |
| `chromium-phone` | 4 | **11.2 s** |

**+11.2 s of spec time.** The PR job measures ~3 minutes wall against a
`timeout-minutes: 10` cap, so this is well inside it. No desktop test
was lost — 42 before, 42 after, confirmed with `--list` per project.

## Scope: a deliberate divergence from the handoff

The handoff on #814 suggests running `sales`, `worker`, `manager`,
`owner`, `named-entity-picker` and `pagination` at phone width. I ran
exactly those six with the fixed `signIn` before choosing: **12 of 19
tests failed and it took 8.5 minutes locally.** Every failure was a
`nav.link()` call against the absent sidebar. Making them pass means
rewriting the nav contract across 18 call sites in 10 files, and it
still yields no assertion a phone can fail that a desktop cannot — so it
could not satisfy the acceptance criterion that is not optional.

So this ships a dedicated `phone.spec.ts` instead. Extending coverage to
real flows at phone width is a reasonable follow-up and is the owner's
call, not something I decided quietly.

## One thing the gate got wrong, and what it cost

**#816 was filed against a phantom and is withdrawn.** An early probe
measured the dashboard at **443/390**, and the reading was real — of
bytes that no longer ship. The sim stack had been up 28 hours, so it was
serving an image built before `7193ebe` (*fix(dashboard): give recent
sales real columns…*, #781, 2026-09-12), the commit that gave that list
its container-query narrow layout. That commit is an ancestor of this
branch's own base, seven commits back.

Rebuilt, with the same `$9,999,999.99` rows still in the fixture:
**390/390**, `grid-template-columns: 179.219px 112px`, widest amount at
x=346.6 inside a 353px panel. Nothing overflows.

So `/` is now **walked** rather than excluded, and the dashboard is
covered at phone width — which is what that issue would have wanted.
Recorded rather than buried, because the rule that would have caught it
is the one this description already quotes: a long-running sim stack
serves the bytes it was built from, not the branch under review.
**Rebuild before believing a rendered measurement**, including one used
only to justify an exclusion.

## Notes for review

- **No screenshots.** This changes no user-visible behaviour: test
harness, one CI comment, and docs. The #662 rule does not trigger.
- **#370 / #565 discipline:** no config key, no boot guard, no harness
input. `bootstrap.sh`, `docker-compose.sim.yml`, `verify-harness.sh` and
the AppHost are untouched.
- The sibling configs (`canary`, `screenshots`, `palettes`) declare
their own `testDir` and `projects` and import none of this, so none
inherits the new project.
- Losing the `@phone` tag does not silently empty the phone project:
`grepInvert` would then let `phone.spec.ts` run at 1280, where the
`phone` fixture refuses to exist. The partition self-guards.
- The action-bar assertion has only 2.2px of margin, and that is stable
rather than fragile: it is `--tabbar-h` (3.6rem) minus `.tab`'s
`min-height` (3.4rem) plus a 1px border, with ~6px of headroom under the
tab's own floor, so CI font metrics cannot move it.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added automated validation for the phone layout at 390×844, including
mobile navigation, accessible touch targets, More menu access,
sticker-bar positioning, and horizontal-scroll prevention.

* **Documentation**
* Updated E2E documentation to describe desktop and phone coverage,
viewport-specific behavior, and mobile navigation conventions.

* **Tests**
* Expanded responsive layout and mutation checks across desktop and
phone screen sizes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
mforce added a commit that referenced this pull request Sep 14, 2026
#865)

## Why

The README's four screenshots were captured on 2026-09-02, before #781
rebuilt both dashboard charts and #791 changed the capture tiles, and
nobody recaptured. Three of the four are refreshed here from a sim stack
rebuilt at `main` (`6c83c5c`), reset and reseeded per the capture
procedure in `tools/simulation/ui/README.md`.

## Scope

- `docs/images/daily-entry.png`, `docs/images/reports.png`,
`docs/images/sales.png`: recaptured by `npm run screenshots`.
- **Not included: `docs/images/dashboard.png`.** Its capture fails the
spec's own guard (`screenshots.spec.ts:108`, bar heights must vary)
because on the simulation fixture every one of the last 14 days is a
partial day: the fixture carries 102 flocks and only two file entries,
so no day is complete, the complete-day peak is null, and every bar is a
2% floor stub with no average. That is the honest picture of the
fixture, and it is not a picture for the README. The dashboard image
therefore stays at its 2026-09-02 state until the fixture or the
partial-day rule changes; see the discussion on the PR.

## Blast Radius

Documentation only. The image job skips (#782); the tracked-file pin
guard and GitGuardian still run.

## Verification

- `bash tools/simulation/reset.sh` at `main`: up, migrated, seeded
(fingerprint `8987971d`), verified.
- `npm run screenshots`: 3 passed, 1 failed (dashboard, as above), and
the three passing captures are the files in this diff.
mforce added a commit that referenced this pull request Sep 14, 2026
## Why

`docs/images/dashboard.png` is the one README image #865 could not
refresh. Its capture fails the spec's own guard (`screenshots.spec.ts`,
bar heights must vary) because on the simulation fixture every day in
the window is a **partial** day: the fixture seeds ~100 catalog flocks
for the picker (#627) which are placed, active and never file, so every
day owes a count nobody filed, `DayStripData.max` is null, and all
fourteen bars render as the 2% floor stub. #865 states that gap and
leaves the image at its 2026-09-02 state, which predates #781's bar
strip and #791.

The product rule is right and the fixture's counts are pinned by the
picker-paging specs, k6 and the e2e suite. So the fixture stays as it is
and the **capture** moves: the sim stack now carries a second, small
farm seeded with the demo profile, and the dashboard image is taken from
that.

Closes nothing — no issue exists. It follows from #865's stated gap.

## Scope

**`seed --profile demo --farm-code <slug>`** (`SeedCliCommand`,
`DemoDataSeeder`). The code is resolved by slug exactly as
`rename-account` and the lifecycle verbs resolve theirs
(`AccountSlugLookup`), after the migrate and before the seed; an unknown
code exits 1 naming `list-accounts`. `DemoDataSeeder.SeedAsync` takes
the target account as an explicit `Guid?` parameter, never ambient state
— `TenantContext` is single-assignment, so a seeder reading the tenant
instead of setting it could only run where somebody else had already
resolved one. Default behaviour is unchanged: every existing caller
passes nothing and gets `SeedDefaults.AccountId`. `--profile simulation`
**refuses** the flag: its manifest, its cast emails and the counts k6
and the e2e suite pin are all default-farm facts, so honouring it would
need a second decision, not a parameter.

Two things came along with that file. Every stderr path in the verb now
routes through one sanitizing sink (#560) instead of two of five,
because the shape where only the messages that quote argv get fixed is
the shape `rename-account` was corrected out of. And the demo seeder's
prerequisite messages no longer say "the default account", which stopped
being true.

**The sim harness** (#370 — all three files considered, and it says so
below). `reset.sh` provisions `readme-farm` with `provision-account`,
rotates its Owner off the printed one-time password onto a stable one,
demo-seeds it, and preflights that the farm is signable and holds the
demo fixture's three flocks. The timezone passed is
`Simulation__TimeZoneId`, not a literal, so the two farms on one stack
cannot end up on different clocks.

- The stable password is generated in `bootstrap.sh` beside
`SIM_ADMIN_PASSWORD` and read back by `reset.sh`, **mirroring the
existing pattern** rather than minting one in `reset.sh`. Generating it
in `reset.sh` would produce a new credential on every reset and leave
`.sim-cast.json` describing the previous one.
- It lands in `.sim-cast.json` under a top-level `readmeFarm` key,
outside the `cast` array, because every entry there signs into
`default-farm` and a driver iterating the cast must not have to ask
which farm each member belongs to.
- The rotation block is now **one shell function with two callers**
rather than two copies of a credential-rotation block.
- Re-running converges. `reset.sh`'s own flow never reaches the
already-exists branch (`down -v` ran at the top), but
`provision-account`'s duplicate behaviour is *not* a no-op like
`bootstrap-admin`'s — it exits 1 with `Provision.SlugTaken*` and prints
no password — so the branch checks the stable credential still signs in
and carries on, and fails loudly on any other failure.
- `verify-harness.sh` fails closed on a missing or blank `README_*`
value and on a cast file that predates the `readmeFarm` key.
- **`docker-compose.sim.yml` needs no change**, and that is a considered
answer rather than an omission: the `README_*` vars carry no `__`,
exactly like `SIM_ADMIN_*`, so they are script-level values `reset.sh`
greps out of `.env.sim` and never app configuration. Nothing new reaches
the container's `environment:` block. For the same reason there is no
`src/Cluckwork.AppHost/Program.cs` change under #565 — no new required
config key exists.

**The e2e suite.** `cast.ts` exposes `readmeFarmOwner()`, whose return
type widens `farmCode` from optional to required; `signIn` and the API
sign-in helper take the code from the member, falling back to
`default-farm`, so every persona written before this one is untouched.
Only the dashboard capture uses it.

**Docs.** `AGENTS.md`, `tools/simulation/README.md` ("Two farms on this
stack" + the `.env.sim` parameter row + the `reset.sh` chain),
`tools/simulation/ui/README.md`, and the dev-database runbook. **No
GLOSSARY or Help change**, deliberately: no user-visible concept changed
— the flag is an operator CLI argument and the second farm exists only
inside the sim harness.

## Blast Radius

`seed --profile demo` with no flag behaves exactly as before, which is
what every existing caller does. `--profile simulation` gains one
refusal on an argument nothing passes today. Per #394 the write contract
is unchanged, so no caller under `tools/simulation/k6/` or `specs/`
needed a change; the one Playwright caller that did (`signIn`) is in
this diff, and `session-races.spec.ts`'s own hardcoded `default-farm` is
correct as written because it drives a sim-cast persona.

The `readme-farm` account exists only in a throwaway `cluckwork-sim`
database. A regenerated `.env.sim`/`.sim-cast.json` is required — run
`bootstrap.sh --force`, then `reset.sh`; `verify-harness.sh` says so by
name if you forget.

Two capture fixes ride along. The #780 readout assertion moved **below**
the screenshot, because focusing a day leaves a focus ring and a readout
balloon that `capture()`'s blur does not dismiss, and the first capture
published both. And the `1280x1180` frame is now held open by the
**Owner's sidebar** (its content ends at 1164px, measured on the
rendered page) rather than by the main column, which on this farm ends
at 700px — anything shorter clips the navigation mid-list. The comment
in `playwright.screenshots.config.ts` says so, because the visible empty
space below the panels otherwise invites a shrink that breaks the
sidebar.

## Verification

Everything below ran in the worktree, against the real stack.

- `dotnet build Cluckwork.sln` — 0 warnings, 0 errors.
- `bash tools/simulation/bootstrap.sh --force` then `bash
tools/simulation/reset.sh` — up, migrated, both farms seeded, all four
preflights green, and the temporary password redacted on both
provisioning paths (checked in the log).
- `cd tools/simulation/ui && npm ci && npm run screenshots` — **4
passed**, including the dashboard capture whose guard fails on the
simulation fixture. `npm run typecheck` clean.
- `SeedCommandTests` (8, up from 5) plus `SimulationSeedCommandTests` —
11 passed. Registry readers found by grepping
`CliDispatcher.Commands|ProcessRoles.OneShotVerbs` under `tests/` rather
than from memory: `CliDispatcherTests`, `OneShotVerbMinimalConfigTests`,
`ProcessRoleRegistryTests`, run with `DemoSeedTests` and
`DemoSeedActorTests` — 29 passed.
- Both `PostgresImagePin_IsOneIdenticalString*` guards run before the
markdown was committed — 2 passed.
- **Mutation-checked, not asserted.** Reverting the CLI's account
routing to `SeedAsync()` turns
`SeedCommand_Demo_WithFarmCode_SeedsThatFarmAndLeavesTheDefaultEmpty`
red. Deleting `readmeFarm` from the cast file, and blanking its
password, each fail `verify-harness.sh` with exit 1; so does a blank
`README_OWNER_EMAIL` in `.env.sim`.
- The new seed test runs against **its own Postgres**, not the class
fixture's, and that is the assertion rather than tidiness: "nothing
landed under the default farm" is only meaningful on a database no
sibling `[Fact]` has demo-seeded, and xUnit guarantees no order within a
class.
- `dotnet test Cluckwork.sln` — result in a comment below.

The `RealSourceTree_AllBypassesAreAllowListed` guard fired on the
`SeedAsync` signature change, which is #632's registry working: the
entry is keyed by the enclosing symbol including its parameters, so
adding one demanded a re-read. The justification is re-written rather
than re-pinned — the `AccountId` predicate on those pre-`tenant.Resolve`
queries is now the caller's account rather than always
`SeedDefaults.AccountId`, and the bypass is still what lets the
preflight see the target farm at all.

## Two judgment calls worth a reviewer's eye

1. **The function is `readmeFarmOwner()`, not `readmeFarm()`.** It
returns a persona, like `owner()` and `restrictedWorker()` beside it,
and `readmeFarm()` reads as though it returns the farm.
2. **The farm name was truncated and is now fixed.** "Meadowlark Farm"
ellipsised to "Meadowlark F…" at the sidebar's 244px; commit 71f95ad
names the farm "Meadowlark" (`README_FARM_NAME` in `bootstrap.sh`) and
recaptures the image after a full reset. The comment below carries the
new capture.

## Screenshot

Before and after below. Same screen, same 1280x1180 frame; the before is
the committed image this PR replaces.

![Before: the committed dashboard image, captured 2026-09-02 on the
simulation fixture, showing the pre-#781 line
chart](https://github.com/user-attachments/assets/aac876b5-49e7-4f91-bcba-6252c01360aa)

![After: the same screen captured from the demo-seeded readme-farm, with
the #781 bar strip showing seven partial and seven complete days, the
average reference line, Avg 803.1 / Peak 822, and one No entry
tile](https://github.com/user-attachments/assets/b24ea479-80bf-46d5-ab6d-2b39bfa98e63)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- The demo seed command can now target a specific farm with `--farm-code
<slug>`.
  - Unknown farm codes return a clear error and guidance.
  - Simulation profiles explicitly reject the farm-selection option.

- **Bug Fixes**
- Sign-in and simulation tooling now correctly support members of
non-default farms.

- **Documentation**
- Updated runbooks and simulation guidance describe multi-farm seeding
and screenshot workflows.

- **Tests**
- Added coverage for targeted seeding, invalid farm codes, and
unsupported simulation options.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: mforce <cleyva@clvc.net>
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.

Dashboard: Recent sales rows do not form columns, and the trend and stock charts are hard to read

1 participant