Repository navigation
fix(dashboard): give recent sales real columns and make both charts readable - #781
Conversation
…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.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 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. Comment |
|
@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. |
|
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 || trueLength 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" HEADLength 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.tsLength 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.cssLength 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.cssLength of output: 21882
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 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.
Local adversarial review found six real defects; pushed
|
| 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.
|
@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. Worth a close look at: the CIE76 |
|
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.tsLength 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.cssLength 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 --shortLength of output: 65636
I found two follow-up items.
The grade-index fix is correct. The CIE76 checks cover grade pairs, semantic colours, palette accents, and both meter surfaces. The direct ✏️ Learnings added
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.
|
Both findings were real and are fixed in P2 — the literal-colour guard. Confirmed: Mutated through the live
P3 — the stale rationale. Confirmed, and it was worse than reported: three files still carried it, not two. 2941 tests green, typecheck and build clean. @coderabbitai review |
|
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' || trueLength 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.cssLength of output: 35768
The DayStrip rationale corrections are consistent across all three files. The ✏️ Learnings added
🧠 Learnings usedYou 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.
|
Real, and fixed in 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
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 |
|
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 --shortLength 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^ 0a01127Length of output: 51001
I found one follow-up item.
🧠 Learnings usedIf 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.
|
Fair, and fixed in Writing them turned up something you did not ask about, and it went the other way. A mutation removing the repeat loop from
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 |
|
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 --shortLength of output: 106947
I found one follow-up item.
🧠 Learnings usedYou 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.
|
Confirmed, and this is the best catch of the run. The cause is worth writing down. The edit lived in a shell step beginning Removed for real in 2961 tests green, typecheck and build clean. @coderabbitai review |
|
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 --shortLength of output: 36298
I found no actionable defects.
The shell inspection stopped after a reference to nonexistent 🧠 Learnings usedYou are interacting with an AI system. |
|
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.cssLength of output: 36039
I found no actionable defect.
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 usedYou are interacting with an AI system. |
🤖 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>
#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.
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 -->
#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.
## 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.   <!-- 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>



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 runAPI, Vite), at 1:1, no downscaling. Same farm, same data, same scenario in every pair.Dashboard at 1440px — before / after
Recent sales in a panel wide enough for four columns — before / after
Recent sales at phone width — before / after
Recent sales
The tracks are sized on the
<ul>and each<li>is agrid-template-columns: subgridrow of it, so the columns align down the list rather than per row. The<li>keeps its own box, so the hairline and thearia-labelare untouched — whichdisplay: contentswould have cost.max-contenton the badge and amount tracks measures whatever the farm's locale renders, so nothing is sized from English (#688). The reference number getswhite-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-gridrewraps. 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
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
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.tsholds 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.tsis 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.--grade-7:=--grade-1)--grade-3GRADE_COLOURS8 → 9 with no ninth token--grade-8from the dark base--grade-8from the dark base, after the fixThe 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 trapDARK_REQUIREDinstyles.test.tsexists 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.tsfails closed on any at-rule context it does not model, and@containerwas 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 matchedborder(-[a-z]+)?, which catchesborder-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.tsasserted the sparkline'spolylinehad 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.tsselects.meter-stack > spanand.dash-list li, both unchanged.No API change, so no write-contract callers (#394). No user-visible concept changes, so
specs/product/GLOSSARY.mdand the Help page are deliberately untouched.Verification
npx vitest run— 124 files, 2934 tests, green.npm run typecheckandnpm run buildclean. 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.