Rebuild the dashboard around drink-window filters - #22
Merged
Merged
Conversation
The page was a sortable table with a search box. On a phone, with 151
bottles, that is a very long scroll and no way to ask the only question
worth asking: what can I drink tonight? The integration has counted
ready_to_drink and past_drink_window since 0.0.18, but the dashboard had
no way to act on either.
* Four filter chips - All wines, Ready to drink, Past window, Needs
aging - each carrying its count, in a sticky header alongside the
search box so neither scrolls away.
* Rows became expandable cards. The summary carries the name, vintage,
location, bin, value and a drink-window bar; tapping opens a drawer
with the barcode, the full window, a Copy bin action and a link to
CellarTracker.
* The window is a position now, not two numbers: a bar showing where
this year sits between BeginConsume and EndConsume, and a pill saying
what to do about it.
* Sort by wine, vintage, value, drink-by, bin or location, either way.
The last year of a window now reads "Drink this year" rather than red.
The page had painted it red since the beginning, which reads as expired;
the coordinator has always counted it as ready. They agree now, and
test_dashboard_agrees_with_sensors runs both implementations over one
cellar to keep them agreeing - a chip contradicting the sensor card
beside it is the integration contradicting itself.
Search covers wine, vintage, location, bin and barcode, and every token
has to match, so "bindi 2023" narrows. Previously it was one substring
test over three fields and a vintage could not be searched at all.
Barcode joins the compact projection to make that real: a field the page
searches but the API never sends is dead code that tests can still
satisfy, so test_every_field_the_page_searches_is_actually_served now
reads SEARCH_FIELDS out of the page and checks it against COMPACT_FIELDS.
Written test-first: 72 new tests over the page's own script under node -
window states, search, sorting, rendering and the drawer. The decision
logic is pure functions at the top level of the script, which is what
makes it callable without a build step; a comment in the page says so.
The fake DOM flattens a document fragment the way a browser does, after
an earlier version nested it and would have passed on a shape no browser
produces.
Also checked by hand in Chromium at 412px and 1000px, light and dark,
which is how the desktop grid turned out to be placing the name in the
middle of the row.
The page grows from 16 KB to 31 KB, about 7 KB gzipped. Still one file,
still no dependencies, still one request.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLPEGFSy3fLEuXNUPAPWR4
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a9ee95d967
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Both reported by Codex on #22, both confirmed before fixing. normaliseWine collapses a missing year to 0, and the missing-value rule only recognised null and "". So 0 sorted as a number smaller than every real year, and sorting by vintage ascending led with every NV bottle in the cellar - the exact thing that rule exists to prevent. Zero now counts as absent for Vintage, BeginConsume and EndConsume, where there is no such year. Valuation is deliberately left alone: a bottle really can be worth nothing recorded, and cheapest-first should still show it. The copy button reported success unconditionally. `navigator.clipboard` requires a secure context, and a great many Home Assistant installs are reached over plain http on a LAN address - so `written` was undefined, the else branch ran, and the button said "Copied" having copied nothing. That is worse than doing nothing: the user walks to the rack and pastes whatever was there before. There is a real execCommand fallback now, and the label follows what actually happened. Finding the second one meant fixing the harness first. It set `globalThis.navigator`, which node 21+ ignores in silence because it ships its own getter-only navigator - so every clipboard fixture was testing the same world and the "missing" case looked like it passed. It uses defineProperty now and throws if the stub does not take. The copy tests assert what reached the clipboard, not just what the button says, so "said Copied" cannot pass for "copied something". 441 tests, ruff clean, mypy clean. Re-checked in Chromium: vintage ascending now ends with the NV bottles, and Copy bin really does put the bin on the clipboard. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LLPEGFSy3fLEuXNUPAPWR4
The README described the page that existed before this branch, and eight
things in it had gone stale - some since 0.0.18:
* "searchable, sortable table" - it is a filtered list now
* the compact view was said to carry nine columns; Barcode made it ten
* "drink-window colouring (green = ready, red = too early or past)" -
too-early is blue and the final year is amber
* the drink-window sensors were missing from the feature list entirely
* the summary card example labelled the diagnostic entity "Connection",
which stopped being true in 0.0.18 when it began reporting a timestamp
* "About drink-window cards" said to sort by the FROM/TO columns, which
no longer exist - and the honest answer is now much better, since the
chips and the sensors both do it
* the test count said 149
* nothing told a contributor that mypy needs Home Assistant installed,
which is the difference between a real check and a silent pass
Two of those are pinned by tests now: the README's column count is read
against COMPACT_FIELDS, and its state labels are read out of cellar.html
itself, so renaming one there fails until the README follows. Both were
confirmed to fail against a sabotaged README rather than assumed to work.
0.0.20 carries the dashboard rebuild and the coordinator's hass cleanup.
The notes lead with the one visible change: a wine in the last year of
its window now reads "Drink this year" in amber rather than red. Red said
"too late" while the sensors counted it as ready, so the page and the
sensors disagreed about the same bottle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLPEGFSy3fLEuXNUPAPWR4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The page was a sortable table with a search box. On a phone, with 151 bottles, that is a very long scroll and no way to ask the only question worth asking: what can I drink tonight? The integration has counted
ready_to_drinkandpast_drink_windowsince 0.0.18, but the dashboard had no way to act on either.431 tests (72 added),
ruffclean,mypyclean, and checked by hand in Chromium.One correction to the brief
The filter chips were not "missing or not rendering" — they had never existed.
git logoncellar.htmlshows a search box and a table from the first commit. That is worth being exact about, because "restore" and "build" are different jobs: there was no regression to find, so this is new work and it needed new tests rather than a fix to old ones.UX critique, and what I did about it
FROM 2027 TO 2032makes the reader do the arithmetic against the current year, per rowLocandBinhad.hide-mobile, so the phone — the device you hold at the rack — was the one that could not see the binTypography and density. Wine names are the scanning target, so they get the weight; everything else steps down to 13px secondary. Prices and years use
tabular-numsso columns of digits line up. On desktop the card collapses to one dense line — name, meta, window, price — because a mouse and 1000px want more rows per screen, not bigger ones.The change that is behavioural, not cosmetic
The last year of a drinking window now reads "Drink this year" instead of red. The page had painted it red since the beginning, which reads as expired; the coordinator has always counted it as ready. They disagreed, and the repo knew —
test_drink_window.pydocuments it as "one deliberate divergence".They agree now, and
test_dashboard_agrees_with_sensors.pykeeps them agreeing: it runs the Python coordinator and the page's JavaScript over the same awkward cellar (inverted windows, single bounds, blanks, both boundary years) and compares the counts. A chip contradicting the sensor card beside it is the integration contradicting itself, and the user has no way to tell which number is wrong.Phase 1 — tests first
tests/dashboard_js.pytests/test_dashboard_filters.pytests/test_dashboard_agrees_with_sensors.pytests/test_dashboard_render.pyAll red before the rewrite. Edge cases the brief asked for are covered explicitly: missing bounds, blank strings,
null,"N/A", negative years, inverted windows, a single-year window (which would otherwise divide by zero), rows missing the sort key, and ties.No Jest or Vitest. The page has no build step, deliberately — it is served straight out of the integration directory into the Companion app. Adding npm to a HACS integration repo would cost more than it returns, and the repo already had a working pattern: extract the
<script>, run it under node. I extended that. The page's decision logic is plain top-level functions with no DOM access, which is what makes them callable directly; a comment incellar.htmlsays so, since it is a testability constraint rather than an accident.Phase 2 — implementation
Vanilla JS, one file, no dependencies, one request.
textContentthroughout — noinnerHTMLtouches server data. OneDocumentFragmentper render, so a 1,000-bottle cellar reflows once.The page grows 16 KB → 31 KB (~7 KB gzipped). Worth stating plainly; it is still negligible beside the inventory payload, and still one request.
BarcodejoinsCOMPACT_FIELDS. A field the page searches but the API never sends is dead code that tests can still satisfy, sotest_every_field_the_page_searches_is_actually_servednow readsSEARCH_FIELDSout of the page and checks it against the projection.Two things I did not build, and why
Notes in the drawer. The brief asked for them. They are free-form prose, potentially long, and PR #18 deliberately kept tasting and cellar notes out of diagnostics for exactly that reason. Shipping every note to every dashboard load is a payload and privacy decision that deserves its own PR; the drawer links to CellarTracker, where the notes live.
"Highlight in Rack" / ESPHome LED. The plumbing is easy — the page already holds a session that can
POST /api/services/.... What is missing is which service: nothing in the integration knows the entity, and inventing one produces a button that fails. Doing this properly means an options-flow field naming the service, then a button rendered only when it is set. Happy to do that as a follow-up; it is a config-surface change, not a dashboard change. Copy bin ships now because it needs no configuration.Interactive verification
The automated tests use a fake DOM, which proves structure and text but not layout. I checked layout in Chromium:
To drive it yourself against a synthetic 151-bottle cellar, serve
cellar.htmlfrom any static server alongside stub JSON at/api/cellartracker/inventoryand/api/cellartracker/settings, then open it at 412px wide. Or install the branch in Home Assistant and open/cellartracker/cellar.html.What I confirmed by hand, at 412px and 1000px, in both colour schemes:
That pass is also what caught the desktop grid placing the wine name in the middle of the row with a hole beside it — the tests were green while it was broken, because a fake DOM has no layout.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LLPEGFSy3fLEuXNUPAPWR4
Generated by Claude Code