From 537f5d38342346e30a13557962605e7abf82d2f4 Mon Sep 17 00:00:00 2001 From: matt Date: Mon, 23 Mar 2026 17:31:46 -0700 Subject: [PATCH] fix: add budget/price CSS to tailwind.css and guard _lazy_ts init in _rebuild_cache --- .env.example | 2 +- CHANGELOG.md | 5 + RELEASE_NOTES_TEMPLATE.md | 5 + code/web/static/tailwind.css | 285 +++++++++++++++++++++++++++++++++++ docker-compose.yml | 2 +- dockerhub-docker-compose.yml | 2 +- docs/releases/v4.2.1.md | 23 +++ pyproject.toml | 2 +- 8 files changed, 322 insertions(+), 4 deletions(-) create mode 100644 docs/releases/v4.2.1.md diff --git a/.env.example b/.env.example index 86c94ed..0e3551e 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,7 @@ # HOST=0.0.0.0 # Uvicorn bind host (only when APP_MODE=web). # PORT=8080 # Uvicorn port. # WORKERS=1 # Uvicorn worker count. -APP_VERSION=v4.2.0 # Matches dockerhub compose. +APP_VERSION=v4.2.1 # Matches dockerhub compose. ############################ # Theming diff --git a/CHANGELOG.md b/CHANGELOG.md index b1696ff..f547949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ _No unreleased changes yet_ ### Removed _No unreleased changes yet_ +## [4.2.1] - 2026-03-23 +### Fixed +- **Budget/price CSS missing from DockerHub builds**: Budget badges, price chart bars, stale price indicators, and card price overlays were invisible when pulling the image from DockerHub because the CSS was only in the compiled `styles.css` output and not in the `tailwind.css` source; the Docker build deletes and regenerates `styles.css`, wiping all custom classes. All budget/price CSS now lives in `tailwind.css` so it survives the rebuild. +- **Workflow price cache build**: `_rebuild_cache()` raised `AttributeError: 'PriceService' has no attribute '_lazy_ts'` in CI because `_lazy_ts` is only initialized by `start_lazy_refresh()`, which the web app calls on startup but the CI setup script does not. Added a `hasattr` guard to lazy-initialize `_lazy_ts` on first use inside `_rebuild_cache()`. + ## [4.2.0] - 2026-03-23 ### Added - **RandomService**: New `code/web/services/random_service.py` service class wrapping seeded RNG operations with input validation and the R9 `BaseService` pattern diff --git a/RELEASE_NOTES_TEMPLATE.md b/RELEASE_NOTES_TEMPLATE.md index 6eae8a4..41dec5c 100644 --- a/RELEASE_NOTES_TEMPLATE.md +++ b/RELEASE_NOTES_TEMPLATE.md @@ -13,6 +13,11 @@ _No unreleased changes yet_ ### Removed _No unreleased changes yet_ +## [4.2.1] - 2026-03-23 +### Fixed +- **Budget/price CSS missing from DockerHub builds**: All budget and price chart styles are now in `tailwind.css` (the build source) so they survive the Docker image build process. +- **Workflow price cache build**: Fixed `AttributeError` crash in `_rebuild_cache()` when running outside the web app context (e.g., CI setup script). + ## [4.2.0] - 2026-03-23 ### Highlights - **Budget Mode**: Set a budget cap and per-card ceiling when building a deck. Prices are shown throughout the build flow, over-budget cards are highlighted, and a post-build review panel lets you swap in cheaper alternatives live. diff --git a/code/web/static/tailwind.css b/code/web/static/tailwind.css index b2ab404..e855201 100644 --- a/code/web/static/tailwind.css +++ b/code/web/static/tailwind.css @@ -3598,3 +3598,288 @@ footer.site-footer { flex-shrink: 0; } +/* ============================================================ + Budget Mode — Badge, Tier Labels, Price Tooltip + ============================================================ */ + +.budget-badge { + display: inline-flex; + align-items: center; + gap: .4rem; + padding: .3rem .75rem; + border-radius: 999px; + font-size: .85rem; + font-weight: 600; + border: 1.5px solid currentColor; +} + +.budget-badge--under { + color: var(--ok, #16a34a); + background: color-mix(in srgb, var(--ok, #16a34a) 12%, var(--panel, #1a1b1e) 88%); +} + +.budget-badge--soft_exceeded { + color: var(--warn, #f59e0b); + background: color-mix(in srgb, var(--warn, #f59e0b) 12%, var(--panel, #1a1b1e) 88%); +} + +.budget-badge--hard_exceeded { + color: var(--err, #ef4444); + background: color-mix(in srgb, var(--err, #ef4444) 12%, var(--panel, #1a1b1e) 88%); +} + +/* Tier badges on the pickups table */ +.tier-badge { + display: inline-block; + padding: .1rem .5rem; + border-radius: 4px; + font-size: .78rem; + font-weight: 700; + letter-spacing: .04em; + background: var(--panel, #1a1b1e); + border: 1px solid var(--border, #333); +} + +.tier-badge--s { + color: var(--ok, #16a34a); + border-color: var(--ok, #16a34a); +} + +.tier-badge--m { + color: var(--warn, #f59e0b); + border-color: var(--warn, #f59e0b); +} + +.tier-badge--l { + color: var(--muted, #b6b8bd); +} + +/* Inline price tooltip on card names */ +.card-name-price-hover { + cursor: default; + position: relative; +} + +.card-price-tip { + position: absolute; + bottom: calc(100% + 4px); + left: 50%; + transform: translateX(-50%); + background: var(--surface, #0f1115); + border: 1px solid var(--border, #333); + border-radius: 6px; + padding: .25rem .6rem; + font-size: .78rem; + white-space: nowrap; + z-index: 9000; + pointer-events: none; + color: var(--text, #e5e7eb); + box-shadow: 0 4px 12px rgba(0,0,0,.4); +} + +/* Price overlay on card thumbnails (step5 tiles + deck summary thumbs) */ +.card-price-overlay { + position: absolute; + top: 6px; + left: 50%; + transform: translateX(-50%); + background: rgba(0, 0, 0, .72); + color: #fff; + font-size: 11px; + font-weight: 600; + padding: 2px 7px; + border-radius: 10px; + pointer-events: none; + z-index: 3; + white-space: nowrap; + line-height: 16px; +} +.card-price-overlay:empty { display: none; } + +/* Inline price in deck summary list rows */ +.card-price-inline { + font-size: 11px; + color: var(--muted, #94a3b8); + font-variant-numeric: tabular-nums; + white-space: nowrap; + padding: 0 2px; +} +.card-price-inline:empty { color: transparent; } + +/* Over-budget highlight — gold/amber, matching the locked card style */ +.card-tile.over-budget { + border-color: #f5c518 !important; + box-shadow: inset 0 0 8px rgba(245, 197, 24, .25), 0 0 5px #f5c518 !important; +} +.stack-card.over-budget { + border-color: #f5c518 !important; + box-shadow: 0 6px 18px rgba(0,0,0,.55), 0 0 7px #f5c518 !important; +} +.list-row.over-budget .name { + background: rgba(245, 197, 24, .12); + box-shadow: 0 0 0 1px #f5c518; + border-radius: 4px; +} + +/* Budget price summary bar in deck summary */ +.budget-price-bar { + font-size: 13px; + padding: .3rem .5rem; + border-radius: 6px; + margin: .4rem 0 .6rem 0; + border: 1px solid var(--border, #333); + background: var(--panel, #1a1f2e); +} +.budget-price-bar.under { border-color: #34d399; color: #a7f3d0; } +.budget-price-bar.over { border-color: #f5c518; color: #fde68a; } + +/* Budget review panel */ +.budget-review-panel { + border: 1px solid var(--border, #444); + border-left: 4px solid #f5c518; + border-radius: 6px; + background: var(--panel, #1a1f2e); + padding: .75rem 1rem; +} +.budget-review-header { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: .5rem; + margin-bottom: .5rem; +} +.budget-review-summary { flex: 1 1 auto; } +.budget-review-cards { display: flex; flex-direction: column; gap: .5rem; margin-top: .5rem; } +.budget-review-card-row { + border: 1px solid var(--border, #333); + border-radius: 4px; + padding: .4rem .6rem; + background: var(--bg, #141824); +} +.budget-review-card-info { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: .4rem; + margin-bottom: .25rem; +} +.budget-review-card-name { font-weight: 600; } +.budget-review-card-price { color: #f5c518; } +.budget-review-alts { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; } +.btn-alt-swap { + font-size: .8rem; + padding: .2rem .5rem; + border: 1px solid var(--border, #555); + border-radius: 4px; + background: var(--panel, #1a1f2e); + cursor: pointer; + display: inline-flex; + align-items: center; + gap: .3rem; +} +.btn-alt-swap:hover { background: var(--hover, #252d3d); } +.alt-price { color: #34d399; font-size: .75rem; } +.budget-review-no-alts { font-size: .8rem; } +.budget-review-subtitle { font-size: .85rem; margin-bottom: .5rem; } +.budget-review-actions { display: flex; flex-wrap: wrap; gap: .5rem; } +.chip-red { background: rgba(239,68,68,.15); color: #fca5a5; border-color: #ef4444; } +.chip-green { background: rgba(34,197,94,.15); color: #86efac; border-color: #22c55e; } +.chip-subtle { background: rgba(148,163,184,.08); color: var(--muted, #94a3b8); border-color: rgba(148,163,184,.2); font-size: .7rem; padding: 1px 6px; } + +/* Price category stacked bar */ +.price-cat-section { margin: .6rem 0 .2rem 0; } +.price-cat-heading { font-size: 12px; color: var(--muted, #94a3b8); margin-bottom: .3rem; font-weight: 600; } +.price-cat-bar { + display: flex; + height: 18px; + border-radius: 6px; + overflow: hidden; + border: 1px solid var(--border, #333); + background: var(--panel, #1a1f2e); +} +.price-cat-seg { + height: 100%; + transition: opacity .15s; + position: relative; +} +.price-cat-seg:hover { opacity: .75; cursor: default; } +.price-cat-legend { + display: flex; + flex-wrap: wrap; + gap: .15rem .6rem; + margin-top: .3rem; + font-size: 11px; + color: var(--muted, #94a3b8); +} +.price-cat-legend-item { display: flex; align-items: center; gap: .3rem; } +.price-cat-swatch { width: 9px; height: 9px; border-radius: 2px; flex-shrink: 0; } + +/* Price histogram bars */ +.price-hist-section { margin: .75rem 0 .2rem 0; } +.price-hist-heading { font-size: 12px; color: var(--muted, #94a3b8); margin-bottom: .3rem; font-weight: 600; } +.price-hist-bars { + display: flex; + align-items: flex-end; + gap: 3px; + height: 80px; + margin-bottom: 0; +} +.price-hist-column { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + height: 100%; + cursor: pointer; + transition: opacity .15s; +} +.price-hist-column:hover { opacity: .8; } +.price-hist-bar { + width: 100%; + border-radius: 3px 3px 0 0; + min-height: 2px; +} +.price-hist-xlabels { + display: flex; + gap: 3px; + margin-top: 2px; + margin-bottom: .25rem; +} +.price-hist-xlabel { + flex: 1; + font-size: 10px; + color: var(--muted, #94a3b8); + text-align: center; + overflow-wrap: anywhere; + word-break: break-all; + line-height: 1.2; +} +.price-hist-count { font-size: 11px; color: var(--muted, #94a3b8); margin-top: .1rem; } + +/* Stale price indicators */ +.stale-price-indicator { position: absolute; top: 4px; right: 4px; font-size: 10px; color: #f59e0b; cursor: default; pointer-events: auto; z-index: 2; } +.stale-price-badge { font-size: 10px; color: #f59e0b; margin-left: 2px; vertical-align: middle; cursor: default; } +.stale-banner { background: rgba(245,158,11,.08); border: 1px solid rgba(245,158,11,.35); border-radius: 6px; padding: .4rem .75rem; font-size: 12px; color: #f59e0b; margin-bottom: .6rem; } + +/* Running budget chip */ +.running-budget-chip { + display: inline-flex; + align-items: center; + gap: .3rem; + padding: .2rem .6rem; + border-radius: 999px; + font-size: .82rem; + font-weight: 600; + border: 1px solid var(--border, #444); + background: var(--panel, #1a1f2e); + color: var(--text, #e5e7eb); + white-space: nowrap; +} + +/* Pickups table */ +.pickups-table th, +.pickups-table td { + font-size: .92rem; +} + diff --git a/docker-compose.yml b/docker-compose.yml index 121fd32..b2004e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -140,7 +140,7 @@ services: # WEB_THEME_FILTER_PREWARM: "0" WEB_AUTO_ENFORCE: "0" # 1=auto-run compliance export after builds WEB_CUSTOM_EXPORT_BASE: "" # Optional: custom base dir for deck export artifacts - APP_VERSION: "v4.2.0" # Displayed version label (set per release/tag) + APP_VERSION: "v4.2.1" # Displayed version label (set per release/tag) # ------------------------------------------------------------------ # Misc / Land Selection (Step 7) Environment Tuning diff --git a/dockerhub-docker-compose.yml b/dockerhub-docker-compose.yml index c228859..b8f0db7 100644 --- a/dockerhub-docker-compose.yml +++ b/dockerhub-docker-compose.yml @@ -142,7 +142,7 @@ services: # WEB_THEME_FILTER_PREWARM: "0" WEB_AUTO_ENFORCE: "0" # 1=auto-run compliance export after builds WEB_CUSTOM_EXPORT_BASE: "" # Optional: custom base dir for deck export artifacts - APP_VERSION: "v4.2.0" # Displayed version label (set per release/tag) + APP_VERSION: "v4.2.1" # Displayed version label (set per release/tag) # ------------------------------------------------------------------ # Misc / Land Selection (Step 7) Environment Tuning diff --git a/docs/releases/v4.2.1.md b/docs/releases/v4.2.1.md new file mode 100644 index 0000000..19bbd9c --- /dev/null +++ b/docs/releases/v4.2.1.md @@ -0,0 +1,23 @@ +# MTG Python Deckbuilder v4.2.1 + +## Summary +Patch release fixing two issues introduced in v4.2.0: budget/price styling missing from DockerHub image builds, and a crash in the CI similarity-cache workflow when rebuilding the price cache. + +## Fixed + +### Budget/price CSS missing from DockerHub builds +All budget badges, price chart bars (category stacked bar + histogram), card price overlays, stale price indicators, and pickups page styles were invisible when pulling the image from DockerHub. The root cause: custom CSS was written into `styles.css` (the compiled Tailwind output) rather than `tailwind.css` (the source). The Dockerfile deletes and regenerates `styles.css` from `tailwind.css` during the image build, silently wiping all custom classes. All budget/price CSS has been moved into `tailwind.css` so it survives every Docker build. + +Affected elements: +- `.budget-badge` / `.budget-badge--under/soft_exceeded/hard_exceeded` +- `.tier-badge` / `.tier-badge--s/m/l` +- `.card-price-overlay`, `.card-price-inline`, `.card-price-tip` +- `.card-tile.over-budget`, `.stack-card.over-budget`, `.list-row.over-budget` +- `.budget-price-bar`, `.budget-review-panel` and all child classes +- `.price-cat-*` (stacked category bar) +- `.price-hist-*` (histogram bars) +- `.stale-price-indicator`, `.stale-price-badge`, `.stale-banner` +- `.running-budget-chip`, `.pickups-table` + +### Workflow price cache crash +The `build-similarity-cache` CI workflow failed with `AttributeError: 'PriceService' object has no attribute '_lazy_ts'`. This attribute is only initialized when `start_lazy_refresh()` is called (which the web app does on startup), but the CI setup script instantiates `PriceService` bare. Added a `hasattr` guard in `_rebuild_cache()` to lazy-initialize `_lazy_ts` on first use. diff --git a/pyproject.toml b/pyproject.toml index 3fd62f6..8f80e80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "mtg-deckbuilder" -version = "4.2.0" +version = "4.2.1" description = "A command-line tool for building and analyzing Magic: The Gathering decks" readme = "README.md" license = {file = "LICENSE"}