Skip to content

feat: shopping list with link previews and price drop alerts - #274

Open
TheBlackSide243 wants to merge 2 commits into
Anyesh:mainfrom
TheBlackSide243:pr/shopping-list
Open

TheBlackSide243 wants to merge 2 commits into
Anyesh:mainfrom
TheBlackSide243:pr/shopping-list

Conversation

@TheBlackSide243

@TheBlackSide243 TheBlackSide243 commented Oct 18, 2026 •

Copy link
Copy Markdown

Description

Adds a Shopping list page for clothes the user wants to buy: a name, notes, a priority, a type,
a price, and either a photo or a shop link. Pasting a link fills in the title, picture and price
from the page. The list also suggests what the wardrobe is short of, by comparing the number of
items per role with a small target (e.g. only one pair of shoes).

Saved links are re-checked once a day (arq cron, 09:05 worker time) and the user gets a notification through
the existing notification channels when a price goes down; a "Check prices" button does it on
demand.

  • wishlist_items table (migrations d7e1f3a9c2b4, e8f2a4b6c1d9), CRUD under /api/v1/wishlist,
    POST /wishlist/preview, GET /wishlist/suggestions, POST /wishlist/check-prices.
  • The link preview is SSRF-safe: public addresses only (checked after DNS resolution and on every
    redirect, including IPv4-mapped IPv6), manual redirects, size and time limits.
  • Frontend page /dashboard/wishlist, nav entry, translations for all 8 locales.

This is one of a set of small, independent PRs (each branches from main and can be reviewed or
dropped on its own); they only overlap in the nav/router registration lines.

Note on migrations

This PR and the declutter one each add a migration on top of a7d3c9e1f5b2. They are
independent, so whichever is merged second needs its first migration's down_revision bumped to
the other's head; happy to rebase it.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • CI/CD or build changes

Checklist

  • I have read the CONTRIBUTING guide
  • My code follows the project's coding style
  • I have added tests that prove my fix/feature works
  • New and existing tests pass locally
  • I have updated documentation as needed
  • My changes don't introduce new warnings or errors

Testing

  • Backend: ruff check, ruff format --check, migration heads check and the full pytest suite on top of current main (2045 passed).
  • Frontend: npm run lint, i18n:check, vocab:check, tsc --noEmit and vitest (693 passed).
  • Running on my own self-hosted instance (Docker Compose on TrueNAS, Ollama).

Test Environment

  • Docker Compose
  • Kubernetes
  • Local development

🤖 Generated with Claude Code

Summary by Sourcery

Add an end-to-end shopping list with wardrobe suggestions, safe shop-link previews, and automated price-drop alerts.

New Features:

  • Add a shopping list for tracking desired clothing items with notes, priorities, prices, links, and photos.
  • Provide link previews that populate item details from shop pages and offer wardrobe-gap suggestions based on existing clothing roles.
  • Track saved shop prices with manual and scheduled checks and notify users about new price drops through existing notification channels.
  • Add a localized wishlist dashboard, navigation entry, and translations across all supported locales.

Enhancements:

  • Harden remote link fetching against SSRF and resource-abuse risks while supporting redirects and product metadata extraction.

Tests:

  • Add coverage for wishlist CRUD, uploads, link previews, SSRF protections, wardrobe suggestions, price tracking, and notification retry behavior.

Chores:

  • Add database migrations and persistence for wishlist items and price-watch state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

This PR introduces an end-to-end shopping-list feature: authenticated wishlist storage and management, SSRF-safe product-link previews, wardrobe-gap suggestions, daily and manual price monitoring with notifications, and a localized dashboard UI backed by migrations, workers, hooks, and focused tests.

Sequence diagram for wishlist link preview and price checking

sequenceDiagram
    actor User
    participant UI as WishlistPage
    participant API as WishlistAPI
    participant Preview as link_preview
    participant Shop as ShopPage
    participant DB as wishlist_items
    participant Worker as PriceWatchWorker
    participant Notify as NotificationDispatcher

    User->>UI: Paste shop link
    UI->>API: preview_link
    API->>Preview: fetch_preview
    Preview->>Shop: GET product page
    Shop-->>Preview: HTML metadata and product data
    Preview-->>API: LinkPreview
    API-->>UI: title, image_url, price
    UI->>API: add_wishlist_item
    API->>DB: Store wishlist item

    User->>UI: Check prices
    UI->>API: check_prices_now
    API->>Preview: check_prices
    Preview->>Shop: fetch_preview
    Shop-->>Preview: Current price
    Preview-->>API: apply_price
    API-->>UI: Updated wishlist entries

    Worker->>DB: due_query
    Worker->>Preview: check_prices
    Preview->>Shop: fetch_preview
    Shop-->>Preview: Current price
    Preview-->>Worker: PriceDrop
    Worker->>Notify: deliver
    Notify-->>User: Price drop notification
Loading

Entity relationship diagram for wishlist price tracking

erDiagram
    USERS ||--o{ WISHLIST_ITEMS : owns
    USERS {
        UUID id PK
    }
    WISHLIST_ITEMS {
        UUID id PK
        UUID user_id FK
        string name
        string link
        decimal price
        decimal current_price
        datetime price_checked_at
        datetime price_dropped_at
        boolean bought
    }
Loading

File-Level Changes

Change Details Files
Adds a user-scoped shopping-list domain with CRUD operations, photo uploads, ordering, and wardrobe-gap suggestions.
  • Introduces the wishlist data model and two migrations for item metadata and price-watch state.
  • Adds authenticated endpoints for listing, creating, updating, deleting, and uploading wishlist entries.
  • Calculates role-based wardrobe gaps from ready, non-archived clothing items.
  • Validates supported clothing types, URL fields, prices, priorities, ownership, and upload limits.
backend/app/api/router.py
backend/app/api/wishlist.py
backend/app/models/__init__.py
backend/app/models/wishlist.py
backend/migrations/versions/d7e1f3a9c2b4_add_wishlist_items.py
backend/migrations/versions/e8f2a4b6c1d9_wishlist_price_watch.py
Implements SSRF-constrained shop-page previews that extract product metadata for wishlist creation.
  • Fetches HTTP(S) pages with manual redirects, public-DNS/IP validation, IPv4-mapped IPv6 handling, response-size limits, and timeouts.
  • Parses Open Graph, Twitter, product metadata, HTML titles, and JSON-LD offers for title, image, price, currency, and site.
  • Exposes an authenticated preview endpoint and rejects invalid or unsafe URLs.
backend/app/services/link_preview.py
backend/app/api/wishlist.py
Adds price monitoring with on-demand checks, daily worker scheduling, and notification delivery for new price lows.
  • Tracks current price, last check, and price-drop timestamps while avoiding repeated notifications for the same low.
  • Limits manual checks and applies delays between shop requests; scheduled checks process due, unbought linked entries under a distributed lock.
  • Groups detected drops by user and dispatches notifications through existing notification channels.
  • Registers the worker and 09:05 cron job.
backend/app/services/price_watch.py
backend/app/workers/price_watch.py
backend/app/workers/worker.py
backend/tests/test_notification_workers.py
backend/tests/test_price_watch.py
Adds the dashboard shopping-list experience and client-side API integration.
  • Provides a localized page for adding entries, reading link previews, uploading photos, marking items bought, deleting entries, viewing price drops, and manually checking prices.
  • Separates open and bought entries, displays totals and wardrobe-gap suggestions, and supports prefilled additions from suggestions.
  • Adds React Query hooks, query keys, navigation links, and translations for all supported locales.
frontend/app/dashboard/wishlist/page.tsx
frontend/lib/hooks/use-wishlist.ts
frontend/lib/hooks/query-keys.ts
frontend/components/sidebar.tsx
frontend/components/mobile-sidebar.tsx
frontend/lib/i18n/locales.ts
frontend/messages/de/nav.json
frontend/messages/de/wishlist.json
frontend/messages/en/nav.json
frontend/messages/en/wishlist.json
frontend/messages/fr/nav.json
frontend/messages/fr/wishlist.json
frontend/messages/it/nav.json
frontend/messages/it/wishlist.json
frontend/messages/ja/nav.json
frontend/messages/ja/wishlist.json
frontend/messages/ko/nav.json
frontend/messages/ko/wishlist.json
frontend/messages/zh-CN/nav.json
frontend/messages/zh-CN/wishlist.json
frontend/messages/zh-TW/nav.json
frontend/messages/zh-TW/wishlist.json
Adds backend coverage for metadata parsing, SSRF protection, wishlist workflows, uploads, authorization, and price updates.
  • Tests Open Graph and JSON-LD parsing, unsafe host rejection, CRUD behavior, uploaded-photo precedence, validation, ownership isolation, preview handling, and wardrobe suggestions.
  • Tests price-drop detection, new-low behavior, clearing recovered prices, first readings, and the manual check endpoint.
backend/tests/test_wishlist.py
backend/tests/test_price_watch.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions github-actions Bot added javascript Pull requests that update javascript code python Pull requests that update python code labels Oct 18, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 7 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="backend/app/services/link_preview.py" line_range="193" />
<code_context>
+        headers={"User-Agent": USER_AGENT, "Accept": "text/html,application/xhtml+xml"},
+    ) as client:
+        for _ in range(MAX_REDIRECTS + 1):
+            await ensure_public_host(urlsplit(current).hostname or "")
+            try:
+                async with client.stream("GET", current) as response:
</code_context>
<issue_to_address>
**Private services become reachable**

When a hostname resolves to a public address during validation and a private address when HTTPX connects, `ensure_public_host` checks one DNS answer, but `client.stream` resolves the hostname again; the preview request can therefore reach an internal service.

Pin the validated address for the connection, or otherwise ensure the connected address is the one that passed the public-host check.
</issue_to_address>

### Comment 2
<location path="backend/app/services/link_preview.py" line_range="193" />
<code_context>
+        headers={"User-Agent": USER_AGENT, "Accept": "text/html,application/xhtml+xml"},
+    ) as client:
+        for _ in range(MAX_REDIRECTS + 1):
+            await ensure_public_host(urlsplit(current).hostname or "")
+            try:
+                async with client.stream("GET", current) as response:
</code_context>
<issue_to_address>
**DNS preflight has no timeout**

When the system DNS resolver is slow or unresponsive, `ensure_public_host` awaits the system resolver without a deadline, and the HTTPX timeout does not cover this earlier lookup. A stalled DNS resolver therefore keeps a preview request waiting indefinitely and can hold a price-check worker job and its distributed lock until the outer worker timeout intervenes.

Put a bounded timeout around the DNS preflight and handle expiry as a `PreviewError`.
</issue_to_address>

### Comment 3
<location path="backend/app/workers/price_watch.py" line_range="43-45" />
<code_context>
+            if user is None:
+                continue
+            try:
+                await dispatcher.deliver(user_id, drop_message(user, user_drops))
+            except Exception as e:
+                logger.warning(f"Price drop notification failed for {user_id}: {e}")
+        logger.info(f"Wishlist prices: checked {len(entries)}, {len(drops)} drops")
+        return {"checked": len(entries), "drops": len(drops)}
</code_context>
<issue_to_address>
**Price-drop alerts disappear**

When a drop is detected and all channels fail, no channels are enabled, or delivery raises, `dispatcher.deliver` results are discarded and raised exceptions are only logged. Because the price state is committed first, later checks do not report the same drop again, so the user never receives the alert.

Record delivery attempts and schedule failed or undeliverable alerts for retry.
</issue_to_address>

### Comment 4
<location path="backend/app/services/price_watch.py" line_range="66" />
<code_context>
+    ]
+    if user_id is not None:
+        conditions.append(WishlistItem.user_id == user_id)
+    return select(WishlistItem).where(and_(*conditions)).order_by(WishlistItem.price_checked_at)
+
+
</code_context>
<issue_to_address>
**New links remain unchecked**

When more than 200 eligible items remain due across daily runs, `due_query` orders by `price_checked_at` ascending, which places unchecked NULL timestamps after dated rows in PostgreSQL. Previously checked rows become due again and stay ahead of new links, so the worker repeatedly selects the same batch and leaves later links unchecked.

Order unchecked rows before dated rows, or otherwise ensure the limited batch advances through all eligible items.

Also at `backend/app/workers/price_watch.py:13`, `backend/app/workers/price_watch.py:28`.
</issue_to_address>

### Comment 5
<location path="backend/app/services/price_watch.py" line_range="44-48" />
<code_context>
+    if entry.price is None:
+        entry.price = price
+        return None
+    dropped = entry.price - price >= MIN_DROP
+    new_low = previous is None or price < previous
+    if dropped and new_low:
+        entry.price_dropped_at = now
+        return PriceDrop(entry, entry.price, price)
+    if not dropped:
+        entry.price_dropped_at = None
</code_context>
<issue_to_address>
**Alerts use stale reference prices**

When the user changes an item's saved price while a scheduled check is fetching its shop page, `apply_price` compares against the previously loaded `entry.price` and uses it in `PriceDrop`, so the worker can report a drop that is no longer warranted and persist monitoring state based on the stale reference.

Reload or lock the item before applying the fetched price, and compare against its current saved reference price.

Also at `backend/app/api/wishlist.py:188`.
</issue_to_address>

### Comment 6
<location path="backend/app/api/wishlist.py" line_range="188" />
<code_context>
+    for key, value in data.model_dump(exclude_unset=True).items():
+        if key in ("name", "priority", "bought") and value is None:
+            continue
+        setattr(entry, key, value)
+    await db.flush()
+    await db.refresh(entry)
</code_context>
<issue_to_address>
**Edited links retain old price state**

When a caller patches an item's link or reference price, `update_wishlist_item` changes the link or price without clearing `current_price`, `price_checked_at`, or `price_dropped_at`. The item can keep showing an outdated drop, compare a new shop against the old shop's price, or wait for the old check interval.

Reset the monitoring state when the item's link or reference price changes.
</issue_to_address>

### Comment 7
<location path="backend/app/services/price_watch.py" line_range="94-100" />
<code_context>
+
+
+def drop_message(user: User, drops: list[PriceDrop]) -> NotificationMessage:
+    italian = (user.locale or "").startswith("it")
+    lines = [f"{d.entry.name}: {_money(d.old)} → {_money(d.new)}" for d in drops[:5]]
+    if italian:
+        title = "Prezzo sceso" if len(drops) == 1 else f"{len(drops)} prezzi scesi"
+        label = "Apri Cosa comprare"
+    else:
+        title = "Price drop" if len(drops) == 1 else f"{len(drops)} price drops"
+        label = "Open shopping list"
+    return NotificationMessage(
</code_context>
<issue_to_address>
**Alerts ignore most locales**

When a user has a supported locale other than Italian, `drop_message` translates alerts only for Italian users and sends English text to everyone else. Users in the other supported locales receive price-drop notifications in English despite the localized shopping-list feature.

Provide translated alert titles, labels, and bodies for all supported locales, with a deliberate fallback for unknown locales.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 7 findings to address first, and the server checks that a URL resolves to public IPs, then httpx resolves it again when connecting; a hostname that changes its DNS answer between those steps could make the server fetch a private service. Reverting stops future fetches, but cannot undo any internal data exposed, and this public-only check is the trust boundary for every preview and price check.

Blocking findings: backend/app/services/link_preview.py:193, backend/app/services/link_preview.py:193, backend/app/workers/price_watch.py:45, backend/app/services/price_watch.py:66, backend/app/services/price_watch.py:48, and 2 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread backend/app/services/link_preview.py Outdated
headers={"User-Agent": USER_AGENT, "Accept": "text/html,application/xhtml+xml"},
) as client:
for _ in range(MAX_REDIRECTS + 1):
await ensure_public_host(urlsplit(current).hostname or "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Critical · Private services become reachable

When a hostname resolves to a public address during validation and a private address when HTTPX connects, ensure_public_host checks one DNS answer, but client.stream resolves the hostname again; the preview request can therefore reach an internal service.

Pin the validated address for the connection, or otherwise ensure the connected address is the one that passed the public-host check.

Prompt for AI agents
In `backend/app/services/link_preview.py` at line 193:

**Private services become reachable**

When a hostname resolves to a public address during validation and a private address when HTTPX connects, `ensure_public_host` checks one DNS answer, but `client.stream` resolves the hostname again; the preview request can therefore reach an internal service.

Pin the validated address for the connection, or otherwise ensure the connected address is the one that passed the public-host check.

Comment thread backend/app/services/link_preview.py Outdated
headers={"User-Agent": USER_AGENT, "Accept": "text/html,application/xhtml+xml"},
) as client:
for _ in range(MAX_REDIRECTS + 1):
await ensure_public_host(urlsplit(current).hostname or "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · DNS preflight has no timeout

When the system DNS resolver is slow or unresponsive, ensure_public_host awaits the system resolver without a deadline, and the HTTPX timeout does not cover this earlier lookup. A stalled DNS resolver therefore keeps a preview request waiting indefinitely and can hold a price-check worker job and its distributed lock until the outer worker timeout intervenes.

Put a bounded timeout around the DNS preflight and handle expiry as a PreviewError.

Prompt for AI agents
In `backend/app/services/link_preview.py` at line 193:

**DNS preflight has no timeout**

When the system DNS resolver is slow or unresponsive, `ensure_public_host` awaits the system resolver without a deadline, and the HTTPX timeout does not cover this earlier lookup. A stalled DNS resolver therefore keeps a preview request waiting indefinitely and can hold a price-check worker job and its distributed lock until the outer worker timeout intervenes.

Put a bounded timeout around the DNS preflight and handle expiry as a `PreviewError`.

Comment thread backend/app/workers/price_watch.py Outdated
Comment on lines +43 to +45
await dispatcher.deliver(user_id, drop_message(user, user_drops))
except Exception as e:
logger.warning(f"Price drop notification failed for {user_id}: {e}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Price-drop alerts disappear

When a drop is detected and all channels fail, no channels are enabled, or delivery raises, dispatcher.deliver results are discarded and raised exceptions are only logged. Because the price state is committed first, later checks do not report the same drop again, so the user never receives the alert.

Record delivery attempts and schedule failed or undeliverable alerts for retry.

Prompt for AI agents
In `backend/app/workers/price_watch.py` at lines 43-45:

**Price-drop alerts disappear**

When a drop is detected and all channels fail, no channels are enabled, or delivery raises, `dispatcher.deliver` results are discarded and raised exceptions are only logged. Because the price state is committed first, later checks do not report the same drop again, so the user never receives the alert.

Record delivery attempts and schedule failed or undeliverable alerts for retry.

Comment thread backend/app/services/price_watch.py Outdated
]
if user_id is not None:
conditions.append(WishlistItem.user_id == user_id)
return select(WishlistItem).where(and_(*conditions)).order_by(WishlistItem.price_checked_at)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · New links remain unchecked

When more than 200 eligible items remain due across daily runs, due_query orders by price_checked_at ascending, which places unchecked NULL timestamps after dated rows in PostgreSQL. Previously checked rows become due again and stay ahead of new links, so the worker repeatedly selects the same batch and leaves later links unchecked.

Order unchecked rows before dated rows, or otherwise ensure the limited batch advances through all eligible items.

Also at backend/app/workers/price_watch.py:13, backend/app/workers/price_watch.py:28.

Prompt for AI agents
In `backend/app/services/price_watch.py` at line 66:

**New links remain unchecked**

When more than 200 eligible items remain due across daily runs, `due_query` orders by `price_checked_at` ascending, which places unchecked NULL timestamps after dated rows in PostgreSQL. Previously checked rows become due again and stay ahead of new links, so the worker repeatedly selects the same batch and leaves later links unchecked.

Order unchecked rows before dated rows, or otherwise ensure the limited batch advances through all eligible items.

Also at `backend/app/workers/price_watch.py:13`, `backend/app/workers/price_watch.py:28`.

Comment thread backend/app/services/price_watch.py Outdated
Comment on lines +44 to +48
dropped = entry.price - price >= MIN_DROP
new_low = previous is None or price < previous
if dropped and new_low:
entry.price_dropped_at = now
return PriceDrop(entry, entry.price, price)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Alerts use stale reference prices

When the user changes an item's saved price while a scheduled check is fetching its shop page, apply_price compares against the previously loaded entry.price and uses it in PriceDrop, so the worker can report a drop that is no longer warranted and persist monitoring state based on the stale reference.

Reload or lock the item before applying the fetched price, and compare against its current saved reference price.

Also at backend/app/api/wishlist.py:188.

Prompt for AI agents
In `backend/app/services/price_watch.py` at lines 44-48:

**Alerts use stale reference prices**

When the user changes an item's saved price while a scheduled check is fetching its shop page, `apply_price` compares against the previously loaded `entry.price` and uses it in `PriceDrop`, so the worker can report a drop that is no longer warranted and persist monitoring state based on the stale reference.

Reload or lock the item before applying the fetched price, and compare against its current saved reference price.

Also at `backend/app/api/wishlist.py:188`.

for key, value in data.model_dump(exclude_unset=True).items():
if key in ("name", "priority", "bought") and value is None:
continue
setattr(entry, key, value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Edited links retain old price state

When a caller patches an item's link or reference price, update_wishlist_item changes the link or price without clearing current_price, price_checked_at, or price_dropped_at. The item can keep showing an outdated drop, compare a new shop against the old shop's price, or wait for the old check interval.

Reset the monitoring state when the item's link or reference price changes.

Prompt for AI agents
In `backend/app/api/wishlist.py` at line 188:

**Edited links retain old price state**

When a caller patches an item's link or reference price, `update_wishlist_item` changes the link or price without clearing `current_price`, `price_checked_at`, or `price_dropped_at`. The item can keep showing an outdated drop, compare a new shop against the old shop's price, or wait for the old check interval.

Reset the monitoring state when the item's link or reference price changes.

Comment thread backend/app/services/price_watch.py Outdated
Comment on lines +94 to +100
italian = (user.locale or "").startswith("it")
lines = [f"{d.entry.name}: {_money(d.old)} → {_money(d.new)}" for d in drops[:5]]
if italian:
title = "Prezzo sceso" if len(drops) == 1 else f"{len(drops)} prezzi scesi"
label = "Apri Cosa comprare"
else:
title = "Price drop" if len(drops) == 1 else f"{len(drops)} price drops"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Alerts ignore most locales

When a user has a supported locale other than Italian, drop_message translates alerts only for Italian users and sends English text to everyone else. Users in the other supported locales receive price-drop notifications in English despite the localized shopping-list feature.

Provide translated alert titles, labels, and bodies for all supported locales, with a deliberate fallback for unknown locales.

Prompt for AI agents
In `backend/app/services/price_watch.py` at lines 94-100:

**Alerts ignore most locales**

When a user has a supported locale other than Italian, `drop_message` translates alerts only for Italian users and sends English text to everyone else. Users in the other supported locales receive price-drop notifications in English despite the localized shopping-list feature.

Provide translated alert titles, labels, and bodies for all supported locales, with a deliberate fallback for unknown locales.

- Link previews connect to the address that passed the public-host check
  (Host header and TLS server name keep the shop's name), so a second DNS
  answer cannot point the request into the LAN; the lookup has a timeout.
- Never-checked links are read before old ones (NULLS FIRST).
- The reference price is re-read after the shop page loads; a changed link
  or a bought entry drops the result.
- Editing the link or the price starts the watch over.
- A drop whose alert could not be delivered is reported again next time.
- Alerts are written in all eight supported languages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update javascript code python Pull requests that update python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants