Repository navigation
feat: packing list for trips - #272
TheBlackSide243 wants to merge 2 commits into
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideAdds an end-to-end trip packing feature: an authenticated backend plans weather- and occasion-aware quantities from the user’s wardrobe, while a localized dashboard lets users generate, inspect, and mark items packed, with forecast fallback and uncovered-role reporting. Sequence diagram for generating a trip packing plansequenceDiagram
actor User
participant PackingPage
participant PackingAPI
participant WeatherService
participant PackingService
participant Wardrobe
User->>PackingPage: submit packing form
PackingPage->>PackingAPI: POST /api/v1/packing
PackingAPI->>WeatherService: geocode_location_name(destination)
WeatherService-->>PackingAPI: coordinates and place
opt trip starts within forecast horizon
PackingAPI->>WeatherService: get_daily_forecast(latitude, longitude, span)
WeatherService-->>PackingAPI: forecast
end
PackingAPI->>PackingService: plan(user, start, end, latitude, forecast, occasion, laundry)
PackingService->>Wardrobe: wardrobe(user)
Wardrobe-->>PackingService: ready wardrobe items
PackingService->>PackingService: summarize_forecast(forecast, start, end, season)
PackingService->>PackingService: plan_quantities(days, weather, laundry, preferences)
PackingService->>PackingService: choose_items(items, plan, weather, season, occasion, preferences)
PackingService-->>PackingAPI: weather, season, packing groups
PackingAPI-->>PackingPage: PackingResponse
PackingPage-->>User: grouped list, forecast or estimate, missing roles
Flow diagram for trip packing plan selectionflowchart TD
A["Trip details: destination, dates, occasion, laundry"] --> B["Geocode destination"]
B --> C{Trip within 16-day forecast horizon?}
C -->|Yes| D["Use daily forecast"]
C -->|No| E["Use seasonal estimate"]
D --> F["Summarize temperatures and rain days"]
E --> F
F --> G["Calculate quantities by wardrobe role"]
G --> H["Score wardrobe items by weather, season, and occasion"]
H --> I["Build grouped packing list"]
I --> J["Flag roles with insufficient suitable items"]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 12 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="backend/app/services/packing_service.py" line_range="98" />
<code_context>
+ temp_min=min(d.temp_min for d in in_trip),
+ temp_max=max(d.temp_max for d in in_trip),
+ rain_days=sum(1 for d in in_trip if is_wet(d)),
+ estimated=False,
+ days=in_trip,
+ )
</code_context>
<issue_to_address>
**Partial forecasts appear complete**
When the trip overlaps available forecast data but includes dates outside it, `summarize_forecast` uses only covered days and sets `estimated=False`, so temperatures, rain counts, and packing decisions omit unforecast days while the response presents the summary as covering the whole trip.
Detect incomplete date coverage in `summarize_forecast` and mark the summary estimated rather than presenting partial data as a full-trip forecast.
Also at `backend/app/services/packing_service.py:90`, `backend/app/services/packing_service.py:97`, `backend/app/api/packing.py:96-99`, `backend/app/api/packing.py:116-120`.
</issue_to_address>
### Comment 2
<location path="backend/app/api/packing.py" line_range="103" />
<code_context>
+
+ weather_service = WeatherService()
+ try:
+ place = await weather_service.geocode_location_name(request.destination)
+ except GeocodingServiceError as e:
+ logger.error(f"Geocoding service error: {e}")
</code_context>
<issue_to_address>
**Geocoding bursts disrupt searches**
When users submit enough distinct uncached destinations to exceed Nominatim’s request limit, `plan_packing` sends uncached destinations to the external geocoder without a shared request limit, so Nominatim throttles requests and packing requests fail with 503s, disrupting destination searches for other users.
Add a shared request throttle or rate limit for geocoding requests.
</issue_to_address>
### Comment 3
<location path="backend/app/api/packing.py" line_range="120" />
<code_context>
+ span = min(FORECAST_HORIZON_DAYS, (request.end_date - today).days + 1)
+ try:
+ forecast = await weather_service.get_daily_forecast(latitude, longitude, max(1, span))
+ except WeatherServiceError as e:
+ # A seasonal estimate still gives a usable list; the response says it is estimated.
+ logger.warning(f"Forecast unavailable for packing: {e}")
</code_context>
<issue_to_address>
**Malformed forecasts cause server errors**
When the weather provider returns invalid JSON or a successful response with an unexpected shape, `get_daily_forecast` lets JSON decoding or response-shape errors escape as exceptions other than `WeatherServiceError`, so this handler does not use its seasonal fallback and the packing request returns a server error.
Validate and normalize the provider response in `get_daily_forecast`, converting decoding and shape failures to `WeatherServiceError` so packing can use its seasonal fallback.
</issue_to_address>
### Comment 4
<location path="backend/app/api/packing.py" line_range="119" />
<code_context>
+ if days_ahead < FORECAST_HORIZON_DAYS:
+ span = min(FORECAST_HORIZON_DAYS, (request.end_date - today).days + 1)
+ try:
+ forecast = await weather_service.get_daily_forecast(latitude, longitude, max(1, span))
+ except WeatherServiceError as e:
+ # A seasonal estimate still gives a usable list; the response says it is estimated.
</code_context>
<issue_to_address>
**Invalid geocodes cause server errors**
When the geocoding provider returns numeric coordinates outside valid latitude or longitude bounds, `get_daily_forecast` raises `ValueError` for out-of-range coordinates, but `plan_packing` catches only `WeatherServiceError`, so the request returns an unhandled server error instead of a controlled response.
Validate geocoded coordinates or handle the forecast service's `ValueError` and return a controlled API error.
</issue_to_address>
### Comment 5
<location path="backend/app/services/packing_service.py" line_range="217" />
<code_context>
+ and_(
+ ClothingItem.user_id == user.id,
+ ClothingItem.status == ItemStatus.ready,
+ ClothingItem.is_archived.is_(False),
+ )
+ )
</code_context>
<issue_to_address>
**Dirty clothes appear packable**
When a ready, non-archived wardrobe item is marked as needing a wash, `PackingService.wardrobe` includes the item without checking `needs_wash`, so the packing groups recommend clothes that are currently dirty.
Exclude items marked as needing a wash when loading the wardrobe for packing.
</issue_to_address>
### Comment 6
<location path="backend/app/services/packing_service.py" line_range="120" />
<code_context>
+ "full_body": 1 if days >= 3 else 0,
+ "footwear": 2 if days > 3 or weather.rain_days > 0 else 1,
+ "mid_layer": 1 if cool_evenings else 0,
+ "outer_layer": 1 if (cool_evenings or weather.rain_days > 0) and not hot else 0,
+ "accessory": 2 if cold else 0,
+ }
</code_context>
<issue_to_address>
**Rainy trips omit outerwear**
When the trip is hot and has at least one rainy day, `plan_quantities` sets `outer_layer` to zero whenever `hot` is true, so `choose_items` skips that role and the packing list omits rain-friendly outerwear.
Allow rainy days to request an outer layer even when the trip is hot.
</issue_to_address>
### Comment 7
<location path="backend/app/services/packing_service.py" line_range="242" />
<code_context>
+ preferences = await self.preferences(user)
+ quantities = plan_quantities(trip_length(start, end), weather, laundry, preferences)
+ groups = choose_items(
+ await self.wardrobe(user), quantities, weather, season, occasion, preferences
+ )
+ return weather, season, groups
</code_context>
<issue_to_address>
**Excluded items get packed**
When a user has saved excluded item IDs that are also ready, non-archived wardrobe items, `PackingService.plan` passes wardrobe items to `choose_items` without filtering `preferences.excluded_item_ids`, so items the user excluded from recommendations can appear in the packing list.
Remove items in `preferences.excluded_item_ids` before choosing packing items.
</issue_to_address>
### Comment 8
<location path="backend/app/services/packing_service.py" line_range="44-51" />
<code_context>
+ "fall": (8.0, 17.0),
+}
+
+ROLE_ORDER = (
+ "outer_layer",
+ "mid_layer",
+ "base_top",
+ "full_body",
+ "bottom",
+ "footwear",
+ "accessory",
+)
+COLD_ACCESSORY_TYPES = {"scarf", "hat"}
</code_context>
<issue_to_address>
**Supported garments disappear**
When the wardrobe contains items assigned to a vocabulary role absent from `ROLE_ORDER`, `choose_items` skips roles absent from `ROLE_ORDER`, and `plan_quantities` does not request some roles such as `suit`; those garments never appear in packing results or missing-role warnings.
Include supported vocabulary roles in the packing role set and plan quantities for roles that need them.
Also at `backend/app/services/packing_service.py:188-189`.
</issue_to_address>
### Comment 9
<location path="backend/app/services/packing_service.py" line_range="171" />
<code_context>
+ elif weather.rain_days and item_type in RAIN_LAYER_TYPES:
+ score *= 1.15
+ if ITEM_ROLE.get(item_type) == "accessory" and item_type not in COLD_ACCESSORY_TYPES:
+ score *= 0.3
+ if item.favorite:
+ score *= 1.1
</code_context>
<issue_to_address>
**Belts can fill cold-accessory gaps**
When the trip is cold and the wardrobe has no suitable hat or scarf but does have a generic accessory that scores at least 0.67 before the penalty, `packing_score` only multiplies non-cold accessories by 0.3, and the resulting score can still meet `MIN_PACKING_SCORE`. In cold weather, a belt or bag can therefore be selected as one of the needed accessories, making the list appear covered without recommending a hat or scarf.
Exclude non-cold accessory types from cold-weather accessory candidates, or keep them from satisfying the cold-accessory requirement.
Also at `backend/app/services/packing_service.py:191`.
</issue_to_address>
### Comment 10
<location path="backend/app/services/packing_service.py" line_range="93" />
<code_context>
+ in_trip = [d for d in forecast if start.isoformat() <= d.date <= end.isoformat()]
+ if not in_trip:
+ low, high = SEASON_TEMPERATURES[season]
+ return TripWeather(temp_min=low, temp_max=high, rain_days=0, estimated=True)
+ return TripWeather(
+ temp_min=min(d.temp_min for d in in_trip),
</code_context>
<issue_to_address>
**Estimated trips report zero rain**
When the trip has no usable in-trip forecast, such as dates beyond the horizon or a forecast-service failure, `summarize_forecast` assigns `rain_days=0` when it has no forecast data, and the API presents that as the trip's rain-day count. For estimated trips this turns unknown precipitation into a dry forecast and can suppress rain gear from the packing list.
Represent precipitation as unknown for estimated weather or use an explicit conservative seasonal estimate instead of reporting zero rain days.
</issue_to_address>
### Comment 11
<location path="backend/tests/test_packing.py" line_range="101" />
<code_context>
+ weather = TripWeather(temp_min=15, temp_max=22, rain_days=2, estimated=False)
+ plan = plan_quantities(2, weather, laundry=False, preferences=None)
+ assert plan["outer_layer"] == 1
+ assert plan["footwear"] == 2
+
+
</code_context>
<issue_to_address>
**Rain layer assertion passes without rain logic**
When the outerwear plan relies only on `cool_evenings`, with no rain-based outerwear rule, `test_rain_adds_a_layer_and_spare_shoes` asserts `outer_layer == 1`, but at 15°C the default 10°C cold threshold makes `cool_evenings` true even without rain. Removing the rain-based outerwear rule therefore still passes this assertion, so the test does not verify that rain adds a layer.
Use a temperature where `cool_evenings` is false and `hot` is false, and compare rainy and dry plans to assert that rain changes the outer-layer count.
</issue_to_address>
### Comment 12
<location path="backend/tests/test_packing.py" line_range="208" />
<code_context>
+ assert groups["base_top"]["needed"] == 3
+ assert "base_top" in data["missing_roles"]
+ packed = [i for g in data["groups"] for i in g["items"]]
+ assert all(not i["is_archived"] for i in packed)
+ assert all(i["image_url"] for i in packed)
+
</code_context>
<issue_to_address>
**Archived-item check never exercises the coat**
When the trip does not request outerwear, as in this test’s dry 16°C forecast, `test_plans_from_the_wardrobe` adds an archived coat, but its dry 16°C forecast produces no outer-layer need. `choose_items` therefore never returns the coat, so even a wardrobe query that includes archived items passes the `all(not i["is_archived"] ...)` assertion.
Make the test require an outer layer and give the coat `status=ready` with `is_archived=True`, so an archived-item leak would cause the assertion to fail.
</issue_to_address>Sourcery assessment
Approval pending. 12 findings to address first.
Blocking findings: backend/app/services/packing_service.py:98, backend/app/api/packing.py:103, backend/app/api/packing.py:120, backend/app/api/packing.py:119, backend/app/services/packing_service.py:217, and 7 more
| temp_min=min(d.temp_min for d in in_trip), | ||
| temp_max=max(d.temp_max for d in in_trip), | ||
| rain_days=sum(1 for d in in_trip if is_wet(d)), | ||
| estimated=False, |
There was a problem hiding this comment.
🟠 High · Partial forecasts appear complete
When the trip overlaps available forecast data but includes dates outside it, summarize_forecast uses only covered days and sets estimated=False, so temperatures, rain counts, and packing decisions omit unforecast days while the response presents the summary as covering the whole trip.
Detect incomplete date coverage in summarize_forecast and mark the summary estimated rather than presenting partial data as a full-trip forecast.
Also at backend/app/services/packing_service.py:90, backend/app/services/packing_service.py:97, backend/app/api/packing.py:96-99, backend/app/api/packing.py:116-120.
Prompt for AI agents
In `backend/app/services/packing_service.py` at line 98:
**Partial forecasts appear complete**
When the trip overlaps available forecast data but includes dates outside it, `summarize_forecast` uses only covered days and sets `estimated=False`, so temperatures, rain counts, and packing decisions omit unforecast days while the response presents the summary as covering the whole trip.
Detect incomplete date coverage in `summarize_forecast` and mark the summary estimated rather than presenting partial data as a full-trip forecast.
Also at `backend/app/services/packing_service.py:90`, `backend/app/services/packing_service.py:97`, `backend/app/api/packing.py:96-99`, `backend/app/api/packing.py:116-120`.|
|
||
| weather_service = WeatherService() | ||
| try: | ||
| place = await weather_service.geocode_location_name(request.destination) |
There was a problem hiding this comment.
🟡 Medium · Geocoding bursts disrupt searches
When users submit enough distinct uncached destinations to exceed Nominatim’s request limit, plan_packing sends uncached destinations to the external geocoder without a shared request limit, so Nominatim throttles requests and packing requests fail with 503s, disrupting destination searches for other users.
Add a shared request throttle or rate limit for geocoding requests.
Prompt for AI agents
In `backend/app/api/packing.py` at line 103:
**Geocoding bursts disrupt searches**
When users submit enough distinct uncached destinations to exceed Nominatim’s request limit, `plan_packing` sends uncached destinations to the external geocoder without a shared request limit, so Nominatim throttles requests and packing requests fail with 503s, disrupting destination searches for other users.
Add a shared request throttle or rate limit for geocoding requests.| span = min(FORECAST_HORIZON_DAYS, (request.end_date - today).days + 1) | ||
| try: | ||
| forecast = await weather_service.get_daily_forecast(latitude, longitude, max(1, span)) | ||
| except WeatherServiceError as e: |
There was a problem hiding this comment.
🟡 Medium · Malformed forecasts cause server errors
When the weather provider returns invalid JSON or a successful response with an unexpected shape, get_daily_forecast lets JSON decoding or response-shape errors escape as exceptions other than WeatherServiceError, so this handler does not use its seasonal fallback and the packing request returns a server error.
Validate and normalize the provider response in get_daily_forecast, converting decoding and shape failures to WeatherServiceError so packing can use its seasonal fallback.
Prompt for AI agents
In `backend/app/api/packing.py` at line 120:
**Malformed forecasts cause server errors**
When the weather provider returns invalid JSON or a successful response with an unexpected shape, `get_daily_forecast` lets JSON decoding or response-shape errors escape as exceptions other than `WeatherServiceError`, so this handler does not use its seasonal fallback and the packing request returns a server error.
Validate and normalize the provider response in `get_daily_forecast`, converting decoding and shape failures to `WeatherServiceError` so packing can use its seasonal fallback.| if days_ahead < FORECAST_HORIZON_DAYS: | ||
| span = min(FORECAST_HORIZON_DAYS, (request.end_date - today).days + 1) | ||
| try: | ||
| forecast = await weather_service.get_daily_forecast(latitude, longitude, max(1, span)) |
There was a problem hiding this comment.
🟡 Medium · Invalid geocodes cause server errors
When the geocoding provider returns numeric coordinates outside valid latitude or longitude bounds, get_daily_forecast raises ValueError for out-of-range coordinates, but plan_packing catches only WeatherServiceError, so the request returns an unhandled server error instead of a controlled response.
Validate geocoded coordinates or handle the forecast service's ValueError and return a controlled API error.
Prompt for AI agents
In `backend/app/api/packing.py` at line 119:
**Invalid geocodes cause server errors**
When the geocoding provider returns numeric coordinates outside valid latitude or longitude bounds, `get_daily_forecast` raises `ValueError` for out-of-range coordinates, but `plan_packing` catches only `WeatherServiceError`, so the request returns an unhandled server error instead of a controlled response.
Validate geocoded coordinates or handle the forecast service's `ValueError` and return a controlled API error.| and_( | ||
| ClothingItem.user_id == user.id, | ||
| ClothingItem.status == ItemStatus.ready, | ||
| ClothingItem.is_archived.is_(False), |
There was a problem hiding this comment.
🟡 Medium · Dirty clothes appear packable
When a ready, non-archived wardrobe item is marked as needing a wash, PackingService.wardrobe includes the item without checking needs_wash, so the packing groups recommend clothes that are currently dirty.
Exclude items marked as needing a wash when loading the wardrobe for packing.
Prompt for AI agents
In `backend/app/services/packing_service.py` at line 217:
**Dirty clothes appear packable**
When a ready, non-archived wardrobe item is marked as needing a wash, `PackingService.wardrobe` includes the item without checking `needs_wash`, so the packing groups recommend clothes that are currently dirty.
Exclude items marked as needing a wash when loading the wardrobe for packing.| ROLE_ORDER = ( | ||
| "outer_layer", | ||
| "mid_layer", | ||
| "base_top", | ||
| "full_body", | ||
| "bottom", | ||
| "footwear", | ||
| "accessory", |
There was a problem hiding this comment.
🟡 Medium · Supported garments disappear
When the wardrobe contains items assigned to a vocabulary role absent from ROLE_ORDER, choose_items skips roles absent from ROLE_ORDER, and plan_quantities does not request some roles such as suit; those garments never appear in packing results or missing-role warnings.
Include supported vocabulary roles in the packing role set and plan quantities for roles that need them.
Also at backend/app/services/packing_service.py:188-189.
Prompt for AI agents
In `backend/app/services/packing_service.py` at lines 44-51:
**Supported garments disappear**
When the wardrobe contains items assigned to a vocabulary role absent from `ROLE_ORDER`, `choose_items` skips roles absent from `ROLE_ORDER`, and `plan_quantities` does not request some roles such as `suit`; those garments never appear in packing results or missing-role warnings.
Include supported vocabulary roles in the packing role set and plan quantities for roles that need them.
Also at `backend/app/services/packing_service.py:188-189`.| elif weather.rain_days and item_type in RAIN_LAYER_TYPES: | ||
| score *= 1.15 | ||
| if ITEM_ROLE.get(item_type) == "accessory" and item_type not in COLD_ACCESSORY_TYPES: | ||
| score *= 0.3 |
There was a problem hiding this comment.
🟡 Medium · Belts can fill cold-accessory gaps
When the trip is cold and the wardrobe has no suitable hat or scarf but does have a generic accessory that scores at least 0.67 before the penalty, packing_score only multiplies non-cold accessories by 0.3, and the resulting score can still meet MIN_PACKING_SCORE. In cold weather, a belt or bag can therefore be selected as one of the needed accessories, making the list appear covered without recommending a hat or scarf.
Exclude non-cold accessory types from cold-weather accessory candidates, or keep them from satisfying the cold-accessory requirement.
Also at backend/app/services/packing_service.py:191.
Prompt for AI agents
In `backend/app/services/packing_service.py` at line 171:
**Belts can fill cold-accessory gaps**
When the trip is cold and the wardrobe has no suitable hat or scarf but does have a generic accessory that scores at least 0.67 before the penalty, `packing_score` only multiplies non-cold accessories by 0.3, and the resulting score can still meet `MIN_PACKING_SCORE`. In cold weather, a belt or bag can therefore be selected as one of the needed accessories, making the list appear covered without recommending a hat or scarf.
Exclude non-cold accessory types from cold-weather accessory candidates, or keep them from satisfying the cold-accessory requirement.
Also at `backend/app/services/packing_service.py:191`.| in_trip = [d for d in forecast if start.isoformat() <= d.date <= end.isoformat()] | ||
| if not in_trip: | ||
| low, high = SEASON_TEMPERATURES[season] | ||
| return TripWeather(temp_min=low, temp_max=high, rain_days=0, estimated=True) |
There was a problem hiding this comment.
🟡 Medium · Estimated trips report zero rain
When the trip has no usable in-trip forecast, such as dates beyond the horizon or a forecast-service failure, summarize_forecast assigns rain_days=0 when it has no forecast data, and the API presents that as the trip's rain-day count. For estimated trips this turns unknown precipitation into a dry forecast and can suppress rain gear from the packing list.
Represent precipitation as unknown for estimated weather or use an explicit conservative seasonal estimate instead of reporting zero rain days.
Prompt for AI agents
In `backend/app/services/packing_service.py` at line 93:
**Estimated trips report zero rain**
When the trip has no usable in-trip forecast, such as dates beyond the horizon or a forecast-service failure, `summarize_forecast` assigns `rain_days=0` when it has no forecast data, and the API presents that as the trip's rain-day count. For estimated trips this turns unknown precipitation into a dry forecast and can suppress rain gear from the packing list.
Represent precipitation as unknown for estimated weather or use an explicit conservative seasonal estimate instead of reporting zero rain days.| weather = TripWeather(temp_min=15, temp_max=22, rain_days=2, estimated=False) | ||
| plan = plan_quantities(2, weather, laundry=False, preferences=None) | ||
| assert plan["outer_layer"] == 1 | ||
| assert plan["footwear"] == 2 |
There was a problem hiding this comment.
🟡 Medium · Rain layer assertion passes without rain logic
When the outerwear plan relies only on cool_evenings, with no rain-based outerwear rule, test_rain_adds_a_layer_and_spare_shoes asserts outer_layer == 1, but at 15°C the default 10°C cold threshold makes cool_evenings true even without rain. Removing the rain-based outerwear rule therefore still passes this assertion, so the test does not verify that rain adds a layer.
Use a temperature where cool_evenings is false and hot is false, and compare rainy and dry plans to assert that rain changes the outer-layer count.
Prompt for AI agents
In `backend/tests/test_packing.py` at line 101:
**Rain layer assertion passes without rain logic**
When the outerwear plan relies only on `cool_evenings`, with no rain-based outerwear rule, `test_rain_adds_a_layer_and_spare_shoes` asserts `outer_layer == 1`, but at 15°C the default 10°C cold threshold makes `cool_evenings` true even without rain. Removing the rain-based outerwear rule therefore still passes this assertion, so the test does not verify that rain adds a layer.
Use a temperature where `cool_evenings` is false and `hot` is false, and compare rainy and dry plans to assert that rain changes the outer-layer count.| assert groups["base_top"]["needed"] == 3 | ||
| assert "base_top" in data["missing_roles"] | ||
| packed = [i for g in data["groups"] for i in g["items"]] | ||
| assert all(not i["is_archived"] for i in packed) |
There was a problem hiding this comment.
🟡 Medium · Archived-item check never exercises the coat
When the trip does not request outerwear, as in this test’s dry 16°C forecast, test_plans_from_the_wardrobe adds an archived coat, but its dry 16°C forecast produces no outer-layer need. choose_items therefore never returns the coat, so even a wardrobe query that includes archived items passes the all(not i["is_archived"] ...) assertion.
Make the test require an outer layer and give the coat status=ready with is_archived=True, so an archived-item leak would cause the assertion to fail.
Prompt for AI agents
In `backend/tests/test_packing.py` at line 208:
**Archived-item check never exercises the coat**
When the trip does not request outerwear, as in this test’s dry 16°C forecast, `test_plans_from_the_wardrobe` adds an archived coat, but its dry 16°C forecast produces no outer-layer need. `choose_items` therefore never returns the coat, so even a wardrobe query that includes archived items passes the `all(not i["is_archived"] ...)` assertion.
Make the test require an outer layer and give the coat `status=ready` with `is_archived=True`, so an archived-item leak would cause the assertion to fail.- A trip only partly covered by the forecast is an estimate: the seasonal range widens the extremes and rain is unknown (null) instead of zero. - Unknown or forecast rain asks for a rain layer, even on a hot trip. - Suits, legwear, socks and ties have a place; suit and tie on formal trips. - Only hats and scarves fill the cold-weather accessory slots. - Items excluded from recommendations stay out; ones needing a wash rank after clean ones. - Malformed forecasts and out-of-range coordinates fall back to the season. - Tighter tests for rain layers and archived items. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Description
Adds a Packing page: pick a destination, the dates, the occasion and whether laundry is
available, and Wardrowbe builds a packing list from the user's own wardrobe, grouped by role
(tops, bottoms, outerwear, shoes...). It uses the daily forecast for the destination when the
trip starts within the 16-day forecast range and a seasonal estimate otherwise (the response says
which). Rain days ask for rain-friendly outerwear; roles the wardrobe cannot cover are flagged.
POST /api/v1/packing(destination is geocoded with the existing location search), no newtables (
services/packing_service.py)./dashboard/packing, nav entry, translations for all 8 locales.This is one of a set of small, independent PRs (each branches from
mainand can be reviewed ordropped on its own); they only overlap in the nav/router registration lines.
Type of Change
Checklist
Testing
ruff check,ruff format --check, migration heads check and the full pytest suite on top of currentmain(2040 passed).npm run lint,i18n:check,vocab:check,tsc --noEmitand vitest (693 passed).Test Environment
🤖 Generated with Claude Code
Summary by Sourcery
Add weather-aware trip packing plans built from each user’s wardrobe.
New Features:
Enhancements:
Tests: