← Back to PaddingPrices
Changelog
All notable changes to PaddingPrices are listed here, newest first.
1.0.353 — 2026-05-08
Added
Five more US scrapers (now 6 total): BambinoDiapers (Shopify, ABDL), ABUniverse (Shopify, ABDL + resold brands), TENA US (Shopify, official US shop), LL Medico (Shopify, incontinence + ABDL — large catalog), XP Medical (Shopify, mixed; pack counts patchy because option2 is often just "case").
Future US candidates investigated: NorthShore Care (EPiServer — needs HTML/sitemap scraping), Rearz.ca (BigCommerce — needs separate API client), HDIS / Carewell / Vitality Medical / Parent Giving (custom platforms or messy Shopify variant naming). These are noted for follow-up; none are easy Shopify-style fits.
1.0.349 — 2026-05-08
Changed
US site domain: `us.prices.diaper.dk` → `usprices.diaper.dk` — Cloudflare's free Universal SSL only covers one level of subdomain (`*.diaper.dk`), so the two-level form (`us.prices.diaper.dk`) failed the TLS handshake at the edge. The flat-subdomain form is covered by the existing wildcard.
1.0.347 — 2026-05-08
Added
Per-region Umami analytics — the Umami `data-website-id` in `index.html` is now a `__UMAMI_WEBSITE_ID__` placeholder that `_get_index_html()` substitutes from the active region's config. EU and US sites now report to separate Umami sites so analytics stay cleanly separated.
1.0.345 — 2026-05-08
Added
Region scaffold for multi-region deployments — introduces a `REGION` env var (`eu` / `us`) that selects scraper set and runtime config. EU scrapers moved to `app/scrapers/eu/`, new empty `app/scrapers/us/` ready for US-store scrapers. New `app/regions.py` defines per-region currencies, default currency, languages, and FX list. New `/api/region` endpoint exposes the active region's config. Frontend now renders the currency switcher dynamically from the region's allowed currencies (and snaps to the region default if a saved currency isn't allowed). Exchange-rate fetcher reads `fx_currencies` from the region (always plus GBP for legacy NRU records). New `docker-compose.us.yml` template for spinning up `us.prices.diaper.dk`. Existing EU site is unchanged at runtime — `REGION` defaults to `eu`.
1.0.344 — 2026-05-08
Changed
Prices now displayed in each shop's native currency — the catalog API now returns `price_per_unit_native` and `currency_native` per listing (matching the price-history endpoint). The frontend uses these for all price display: product detail table, list view, compare view, and cart. Conversion to the selected display currency is done client-side via live exchange rates using the cross-rate formula (native → EUR → display). This means a DKK store shows "49.95 DKK" and a GBP store shows "12.99 GBP" rather than both being shown as EUR-converted values, giving more accurate per-unit comparisons. Sort order is unchanged (still EUR-based for consistent ranking).
1.0.337 — 2026-05-07
Added
Out-of-stock badge on trends rows — when a product's latest price record is marked out of stock, the trends row now shows a red "Out of stock" badge next to the product name, the row is dimmed (65% opacity), and clicking it is disabled. The badge text is localised in all five languages.
1.0.334 — 2026-05-07
Fixed
Trends page rows not clickable — the `/api/price-trends` endpoint now returns canonical brand/model names (same pipeline as the catalog: `_normalise_brand`, `clean_model_name`, brand-prefix stripping, BetterDry/Crinklz canonicalization). Previously the raw DB values (`"TENA"`, `"Crinklz Aquanaut"`) never matched the catalog's display values (`"Tena"`, `"Aquanaut"`), so `clickable` was always false. Clicking a trend row now opens the product detail page on PaddingPrices.
Brand casing inconsistencies between catalog and trends — added `Tena`, `MoliCare`, and `Id` to `_BRAND_CANONICAL` so scrapers that store `"TENA"`, `"Molicare"`, or `"iD"` resolve to the same display name as those storing `"Tena"`, `"MoliCare"`, or `"Id"`. Ensures brand-level grouping and trends-row catalog lookup are consistent.
1.0.327 — 2026-05-04
Fixed
"Log ind for at synkronisere…" note shown when already logged in — `applyI18n()` runs before the `/api/me` auth check completes, so the note always rendered. Now hidden immediately after auth state resolves (logged in → hide, logged out → show); also correctly reappears on logout.
1.0.327 — 2026-05-04
Security
File permissions tightened: production `paddingprices.db` and `.env` changed from `644` to `600` — no longer world-readable on the server.
Email redacted in logs: price-alert log entries now show `us***@example.com` instead of the full address, so plaintext emails no longer accumulate in the log file.
Constant-time password comparison: `POST /login` and `POST /admin/login` now use `hmac.compare_digest` instead of `==`, eliminating the timing-attack vector on the invite and admin passwords.
Security headers added: all responses now include `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin`, `Strict-Transport-Security` (1 year, includeSubDomains), and a `Content-Security-Policy` that restricts scripts/styles/images to self and the Umami analytics origin.
1.0.329 — 2026-05-04
Changed
Catalog shows both polybag and full-case listings from the same store — the per-size dedup key is now `(store, pack_count)` instead of `store`. Stores that sell the same product in multiple pack sizes (e.g. BetterDry.com's 15-piece polybag and 60-piece case) now show both options side-by-side so buyers can compare the commitment vs. convenience trade-off. True duplicates (same store, same pack count, different URLs) are still collapsed to the cheapest.
1.0.326 — 2026-05-03
Fixed
Cloudrys: 13 remaining no-PPU items after v1.0.325 — added three more vendor defaults: `Tena` (14, standard Ultima Active Fit pack), `Schnuller` (1, individual pacifiers), `Lil Comforts` (1, single-unit Rearz accessories).
MaxDiaper ABU Simple Daytime: wrong pack_count=1 — the detail page carries no pack-count text; patched the two DB rows (M/L) directly to `pack_count=10` (ABU standard) since the scraper cannot infer it from the page.
1.0.325 — 2026-05-03
Fixed
Cloudrys: brand-default pack counts silently ignored for many vendors — `_BRAND_DEFAULT_PACK` was keyed by canonical brand names, but Cloudrys Shopify returns vendor names with different capitalisation (e.g. `"Betterdry"` instead of `"BetterDry"`, `"Northshore"` instead of `"NorthShore"`). Now uses a case-insensitive lookup (`_BRAND_DEFAULT_PACK_LC`). Also added missing vendors: `Beyond`, `CutieplusU`, `Forsite`, `Kiddo`, `LFB`, `PupStyle`.
MaxDiaper: stale `pack_count=1` cache entries prevent correct re-fetch — the cache bypass condition now rejects any batch of `#sz=` variants where all stored `pack_count` values are `1` (a sentinel for "unknown"), forcing a fresh detail-page fetch so the `_ANTAL_I_FORPACKNING_RE` pattern introduced in v1.0.323 can apply correctly (e.g. ABU PeekABU now gets `pack_count=10`). Single-size cached entries are similarly only trusted when `pack_count > 1`.
1.0.323 — 2026-05-03
Fixed
ABDL-TB-Club-Shop: packaging-only products show wrong price — products with a Packaging (Single/Pack) attribute but no Size attribute (e.g. Tena Slip Junior) fell back to the listing-page price because `_parse_variants` returned empty when no size attribute was found. Now correctly extracts the Pack option's price from the Magento jsonConfig `optionPrices`.
MaxDiaper: pack_count=1 for some ABU/Tykables products — `_parse_pack_sv` matched "1 st" (the cart quantity selector) before finding "Antal i Förpackning 10" in the data sheet. Added a high-priority `_ANTAL_I_FORPACKNING_RE` pattern that matches the MaxDiaper product attribute format first; also guards all pack-count results against values < 2 to reject cart quantities.
Europe-Incontinence: no pack count for products with "all-in-one briefs" — `_PACK_RE` used `\w+` for intermediate adjective words, which broke on hyphenated compounds like "all-in-one". Changed to `[\w-]+` to handle hyphens.
Cloudrys: 253 no-PPU listings for products without "N Stück" in title — added fallback pack-count patterns for descriptions ("N Stück im Paket", "Pack of N"), `known_pack_counts` cache lookup, and a brand-default map for standardised ABDL brands (ABU=10, BetterDry=15, Crinklz=15, Rearz=12, Tykables=10, NorthShore=10, etc.).
NRU: 201 clothing/gear listings with no price-per-unit — clothing and gear items are sold individually; now defaults `pack_count = 1` for those categories when no count is found in the product description.
CuddleKingdom: accessories and clothing with no price-per-unit — non-diaper products (clothing, change pads, gift cards, accessories) now default to `pack_count = 1` when no Dutch/English pack-count pattern is found in the description.
1.0.322 — 2026-05-03
Fixed
ABDL filter missing Cutieplusu: `CutiePlusU` in the ABDL brand set did not match the server-normalised name `Cutieplusu`, so Cutieplusu products always showed in Medical mode and were never filtered. Corrected to match the catalog's canonical casing.
Phantom "Dalille" ABDL brand: `Dalille` was incorrectly listed as an ABDL brand; it is a store, not a brand. Removed from the ABDL set so medical-mode filtering is no longer affected.
1.0.318 — 2026-05-03
Fixed
Price history graphs show FX noise in non-EUR currencies: price records stored `price_eur` calculated using the exchange rate at scrape time; displaying in DKK multiplied by the *current* rate, so a stable DKK price appeared to fluctuate whenever EUR/DKK moved. The price history API now also returns `price_per_unit_native` and `currency_native` (the original scraped price and currency). Charts use the native price directly when the display currency matches (e.g. DKK user viewing DKK-priced stores), or convert via the current cross-rate otherwise — eliminating historical FX noise for all stores that price in DKK, EUR, SEK, NOK, USD or GBP.
1.0.316 — 2026-05-03
Added
ABDL / Medical use-case filter: new 3-way toggle (All / ABDL / Medical) in the filter bar. ABDL mode shows only brands that are primarily or exclusively ABDL-focused; Medical mode shows everything else. Selection persists in localStorage. Works across brand grid, model grid, and flat list view.
1.0.313 — 2026-05-03
Fixed
Product hero image showing broken/hidden: Diaper-Minister serves product images from their own domain with hotlink protection — any external embed returns 403. After a DM re-scrape triggered by the v1.0.305 pack-count migration, the DM image URL was stored and started being picked as the representative model image, causing it to be silently hidden (`onerror`). Added `_is_embeddable_image()` helper to skip known hotlink-blocked domains when selecting the model image; Diaper-Minister CDN-less URLs are now excluded. DB migration clears all stored `diaper-minister.com` image URLs so the next scrape doesn't re-introduce them.
1.0.311 — 2026-05-02
Changed
Comment layout compact: note text, timestamp, and edit/delete buttons now all appear on a single line per comment instead of stacking vertically. Long note text is truncated with ellipsis.
1.0.309 — 2026-05-02
Added
Timestamped private comments: the note panel on each product page now stores individual comments with creation and last-edited timestamps, displayed below the text. Each comment can be edited inline (timestamp updates to edit time) or deleted. Multiple comments are supported per product.
Backend comment API: `PUT /api/me/notes` accepts an optional `comment_id` to edit a specific comment; `DELETE /api/me/notes` accepts `comment_id` to remove a single comment. Comments are stored as a JSON array in the existing `user_notes.content` column; legacy plain-text notes are migrated transparently on first read.
1.0.307 — 2026-05-02
Fixed
Diaper-Minister per-size pack count still reading wrong value: replaced the fixed 500 ms wait after `select_option` with `wait_for_load_state("networkidle")` so the data sheet is read only after all AJAX combination-update requests have settled. Added debug logging to detect whether the data sheet is dynamic (updates per size) or static on any given product.
1.0.305 — 2026-05-02
Fixed
Diaper-Minister per-size pack counts not applied to cached products: after the v1.0.303 fix, newly-scraped products got correct per-size pack counts, but products already in the DB with uniform (wrong) values were served from cache indefinitely. A one-time DB migration (guarded by `migration_flags` so it only runs once) clears all DM size-variant pack counts on next deploy, forcing a fresh detail-page fetch for every product. Subsequent scrapes use the correct per-size values.
1.0.303 — 2026-05-02
Fixed
Diaper-Minister pack count wrong when it varies by size: pack count was parsed once from the initial page load and then applied uniformly to every size variant. Products like Rearz Lil Bella (M=16, L=12, XL=16 units) got the wrong count. The data sheet now re-read after each size selection so each variant gets its own pack count.
1.0.301 — 2026-05-02
Fixed
Cloudrys products not appearing in catalog: all Cloudrys standard products are 2-packs ("2 Stück …"), but the scraper was treating any pack count < 10 as a trial pack and skipping them. Threshold lowered — only genuine single-unit listings (pack_count = 1) are skipped now. Products show correct per-unit cost based on the 2-pack price.
Cloudrys model names containing trailing "Windeln": the German store appends "Windeln" (diapers) to all non-own-brand diaper titles (e.g. "Rearz Lil Bella Windeln"). `_parse_model` now strips trailing "Windeln"/"Windel" so models match across stores. DB migration cleans up stale records.
Cloudrys model names containing trailing vendor name: some titles have the brand at the end instead of the start (e.g. "Daydreamer Rearz"). `_parse_model` now also strips a trailing vendor-name match. DB migration fixes existing records.
Cloudrys model names with orphaned trailing punctuation: stripping suffixes sometimes left a trailing " -" (e.g. "Abri Form -"). Final RTRIM pass removes leftover punctuation. DB migration cleans up stale records.
1.0.299 — 2026-05-02
Fixed
Cloudrys model names containing "mit Folie" not stripped: the regex `\S+folie` required at least one non-space character before "folie", so "Plastikfolie" was stripped but standalone "Folie" was not. Changed to `\w*folie` (zero or more word chars) to handle both forms. DB migration also cleans up stale records already stored with the "mit Folie" / "mit Plastikfolie" suffix.
1.0.297 — 2026-05-02
Fixed
Lil Bella Baby Bottle / Change Pad appearing as own brand: Rearz "Lil Bella" is a product line, not a brand. Both products now appear under Rearz with model names "Lil Bella Baby Bottle" and "Lil Bella Change Pad", categorized as gear.
Critter Caboose appearing as own brand or split into "Critter": Rearz "Critter Caboose" was stored as brand="Critter" (SaveExpress, first-word parse) and brand="Critter Caboose" (CuddleKingdom). Both now correctly land under Rearz.
Aquanaut appearing as own brand instead of Crinklz: stale DB records had brand="Aquanaut" for the Crinklz Aquanaut Windelhose. Fixed via DB migration; future scrapes already return brand="Crinklz".
\*Sonderpreis\* brand with redundant "Bambino" in model name: existing migration fixed the brand but not the model. Now also strips the "Bambino " prefix from model_name so products appear as "Bambino > Teddy V2 WindelSlip" instead of "Bambino > Bambino Teddy V2 WindelSlip".
`bottle` and `change pad` keywords added to `_RE_GEAR`: English bottle and changing-pad product names are now correctly classified as gear rather than other.
1.0.293 — 2026-05-01
Fixed
Size filter hid models with non-standard sizes: the "no size" check only caught null/empty, but size-less products use `"—"` as their size value. Any size not in the XS/S/M/L/XL/XXL/XXXL dropdown (including `"—"`, `"ONE SIZE"`, `"S/M"`, `"M1"` etc.) now always passes through the size filter in both the brand/model grid and the list view.
1.0.291 — 2026-05-01
Added
Size filter next to capacity slider: the size dropdown has been moved from the list-view toolbar into the capacity row, making it visible in all views (brands, models, product, and list). Selecting a size now filters the brand grid and model list to only show products available in that size.
Fixed
Size filter hiding size-less listings in table view: products with no size information (e.g. single-pack accessories) were incorrectly hidden when a size filter was active. They are now always shown.
1.0.289 — 2026-05-01
Fixed
Id Expert Belt Super Large showing ~€12/unit: ABDL-TB-Club-Shop scraper failed to find pack count for products using the "Amount Pack: N" spec table format. The regex only matched keywords like "pieces/pcs/diapers" — a bare "14" under "Amount Pack" was ignored and the fallback found "1 Item" (cart indicator) instead. Fixed by adding `_amount_pack_re` pattern and raising the minimum matched value from 1 to 2 (skipping "1 item" cart counters). All similarly affected products (Attends, Crinklz, Rearz, Seni, Id Pants/InnoFit) will be corrected on the next scrape.
ABDL clothing and gear items (onesies, pacifiers, bottles, mittens) categorized as `aio`: brand catch-all rules in `_STATIC_DEFAULTS` (LittleForBig → aio, Little → aio, ABU → aio, Tykables → aio) fired before clothing/gear keyword patterns. Restructured `classify()` so `_RE_CLOTHING` and `_RE_GEAR` are checked first, before any static brand defaults. Affected products now correctly classified: ABU PeekABU Mössa (clothing), Little Darling Overall Shorts / Blue Furrytail Racecar Romper Bodysuit / Blue Onesie Bodysuit (clothing), Little Blue Bottle Nappflaska (gear), LittleForBig VANTAR HOT PINK / BABY ELEFANT NAPP / VUXEN NAPPFLASKA BABY PARAD (clothing/gear).
Blöjbyxa, Gullig, Pvc products categorized as `other`: Blöjbyxa (diaper covers/plastic pants), Gullig T-SHIRT, and Pvc BYXA products had no matching keywords. Added `blöjbyxa`, `byxa`, `vantar`, `overall[s]?`, and `t[-\s]?shirt` to `_RE_CLOTHING`. All three brands now resolve to `clothing`.
Abdry / Astro / Goth / Baby Usagi ABDL diapers categorized as `other`: these MaxDiaper brands had no static rules and no matching keywords. Added catch-all `aio` rules for Abdry, Astro, Goth, and Baby Usagi.
1.0.287 — 2026-05-01
Fixed
Attends Contours Air Comfort categorized as `aio`: Attends Contours are shaped anatomical pads worn inside fixation pants → now `pad`.
1.0.285 — 2026-05-01
Fixed
Strampelpeter Flockenwindeln categorized as `aio`: these are disposable booster/insert pads — now `booster`.
Seni Kids Junior in catalog: Seni Kids Junior (Extra/Super) are children's incontinence products — now excluded as `baby`.
Seni Soft Krankenunterlagen shown as `aio`: Seni Soft Basic/Super are medical bed underlays — now `other`.
Windelhose variants categorized as `aio`: "Windelhose/Windelhosen" added to `_RE_PULLUP`; brand-specific overrides added for Forsite, Tykables, LittleForBig, and Little (ABU). Affected products: Forsite AM PM Stars/Stripes Windelhose, Tykables Little Builders Windelhose, Little Rascals Windelhose, Molicare Slip Windelhosen, Omutsu Purrfection Windelhose.
Abena Krankenunterlage shown as `pad`: was incorrectly matched by the "light" shaped-pad rule. Added explicit `("abena", "krankenunterlage", "other")` rule before the light rule.
Dailee Pant Lady Plus categorized as `pad`: "lady" keyword in `_RE_PAD` was firing before the "pant" pullup keyword. Added `("dailee", "pant", "pullup")` static rule.
Id Comfy Junior Pants 8-15 years in catalog: age range "8-15 years" confirms this is a children's product — now excluded as `baby`.
`unterlage[n]?` and `krankenunterlage[n]?` added to `_RE_OTHER`: ensures bed underlays from brands without explicit rules are correctly categorized.
1.0.283 — 2026-05-01
Fixed
Attends Soft (Mini 0, Mini Long 1, Extra Plus 3) shown as tape brief: Attends Soft line are shaped incontinence pads — added `("attends", "soft", "pad")` static rule. Attends Cover-Dri bed underlays now correctly classified as `other`.
Children's diapers in catalog: Libero (Comfort 3–7, Newborn, Up & Go…) and Bambo Nature are exclusively children's brands and are now excluded from the catalog entirely. The catalog builder skips any product classified as `"baby"`.
1.0.281 — 2026-05-01
Fixed
Table view: pressing Back no longer returns to table view: list view is now represented in the URL hash (`#/list`) so the browser history stack includes it. Pressing Back from a product page correctly restores the table view.
Table view: models cannot be opened in a new tab: the model name cell in the table now renders as a proper `<a href="#/Brand/Model">` anchor, enabling right-click → "Open in new tab/window".
Many products categorized as `other` instead of `aio`/`pad`/`pullup`: comprehensive categorization QA pass. Added static rules for Attends, Bambino, Forsite, Kiddo Diaper, Kolibri, Libero, Medi-Inn, NRU, Rearz, Strampelpeter and Invizi. Extended `_RE_AIO` to also match "Slips" (German/English plural). Rearz Overnight Booster Pads now correctly categorized as `booster`.
Seni miscategorizations: Seni Control (Mini–Super) and Seni Lady/Lady Slim → `pad`; Seni Active, Lady Pants, Man Pants → `pullup`; Seni Lady and Man (non-Pants) → `pad`; Seni R Pads, V Maxi/Normal, San, Vorlage lines → `pad`; Seni Care skin-care products → `other`.
Tena Comfort categorized as `other`: Tena Comfort (Normal/Plus/Extra/Super/Ultima) are shaped anatomical pads → `pad`. Tena Silhouette → `pullup`. Tena Men Active Fit Protector → `pad`.
LittleForBig Training Pants categorized as `aio`: Training Pants variants now correctly classified as `pullup`.
1.0.279 — 2026-05-01
Fixed
Category filter not persisted across page refreshes: the migration that re-enables newly added categories was re-adding *all* categories on every refresh, overwriting user preferences. Fixed by storing the set of known categories in `localStorage` (`pp_known_categories`) and only auto-enabling a category when it is genuinely new (not seen in any previous session).
1.0.277 — 2026-04-28
Fixed
ABDL-TB-Club single-unit products shown as pack price: `_parse_variants` now also collects "Single" packaging variants and sets `pack_count=1`. Products only available as single units (like TENA Slip Super Original semi-plastic-backed L) now appear in the catalog with the correct per-unit price display instead of an unlabeled pack price.
Catalog hid all pack_count=1 products: removed the blanket `pack_count != 1` filter. SaveExpress einzelstueck and CuddleKingdom "los" are already guarded by URL-pattern filters.
Zero prices in price-history graph: `/api/price-history/products` now filters out `price_eur=0` sentinel records (OOS markers), matching the behaviour of the single-product endpoint.
Scheduler: products with no price history purged instead of getting a zero marker: when `last is None` (all records were deleted), the orphan product is removed rather than receiving a new `price_eur=0` OOS sentinel that would perpetuate the zero-price cycle.
1.0.275 — 2026-04-28
Fixed
Zero prices for out-of-stock products: OOS marker in the scheduler wrote `price_eur=0` as a sentinel, which the catalog then displayed as "€0" instead of a crossed-out last-known price. Scheduler now copies the last known price into the OOS marker record so the catalog shows the real price with an OOS badge. Deletion logic updated accordingly (triggers on `in_stock=0` from the previous run rather than `price_eur=0`).
Stale zero-price records purged on startup: DB migration deletes all `price_eur=0 / in_stock=0` price_records for products that have no real (non-zero) price history, and removes orphaned product rows.
1.0.273 — 2026-04-27
Fixed
TENA Slip Active Fit miscategorized as pull-up: `active fit` keyword was in `_RE_PULLUP` but TENA Slip Active Fit is a taped brief (AIO). Removed the keyword and added TENA Slip/Flex static defaults in `categorize.py` so they are reliably classified as AIO.
"Slip Active Fit Plus" merged with "Slip Active Fit Maxi": fuzzy-clustering threshold raised from 80 → 85 to prevent the 80.0-scoring Plus/Maxi pair from collapsing into a single catalog entry.
"Slip Utima" typo: TENA Sverige website spells the product "Utima" (missing L). DB migration corrects stored `model_name` to "Slip Ultima" on startup.
1.0.271 — 2026-04-26
Added
TENA Sverige scraper (`app/scrapers/tena_nu.py`): scrapes `www.shop.tena.nu` via sitemap XML (≈79 products). Parses Angular SSR transfer-state JSON embedded in each product page to extract name, size, pack count, price (SEK), and stock status. Skips bulk carton variants; strips creams, soaps, shampoo caps, and bed-protection products. Registered in `ALL_SCRAPERS`.
1.0.269 — 2026-04-26
Fixed
Diaper-Minister missing categories: `cloth-backed-diapers`, `abdl-training-pants`, `plastic-diapers`, `boosters`, `washable-diapers` and the post-restructure slug forms of existing categories are now in `CATEGORY_URLS`. Products in those categories (ABU, Tykables, Lil Comforts, Crinklz, etc.) will get properly re-scraped with correct size variants on the next run. Duplicate-URL guard already in place so products appearing in multiple categories are scraped only once.
1.0.267 — 2026-04-26
Fixed
Diaper-Minister "Teddy Bums" brand: Product was stored as brand "Teddy", model "Bums" before the slug-override `teddy-bums → Lil Comforts` was in place. DB migration corrects it to brand "Lil Comforts", model "Teddy Bums".
1.0.265 — 2026-04-26
Fixed
Rearz/Crinklz print names appearing as brand: Several stores store the print/model name as the brand (Alpaca, Daydreamer, Astronaut, Buccaneer, Fairytale). Added to `_BRAND_CANONICAL` so they resolve to Rearz or Crinklz at catalog-build time. Also corrected `_FIRST_WORD_IS_MODEL` in the SaveExpress scraper — Astronaut, Buccaneer and Fairytale are Crinklz prints, not Rearz.
1.0.263 — 2026-04-26
Fixed
Einzelstueck single-unit listings hidden: SaveExpress "einzelstueck" products have no pack count so they slipped past the `pack_count != 1` guard. Catalog query now excludes any URL containing `einzelstueck`, and `einzelstueck` is added to the model-name noise strip so it doesn't pollute model keys.
1.0.261 — 2026-04-26
Fixed
Abri-Flex / Abri-San / Abri-Let etc. now group under Abena: Several stores (SaveExpress, CuddleKingdom, Ugleapo, Europe-Incontinence) store Abena product-line names as the brand (`Abri-Flex`, `Abri-San`, `Abri-Let`, `Abri`, …). Added all variants to `_BRAND_CANONICAL` so they normalise to Abena at catalog-build time without touching the DB.
1.0.259 — 2026-04-26
Fixed
Children's diapers excluded from catalog: Pampers, Huggies, and Bebe products (from SaveExpress, CuddleKingdom, MaxDiaper) no longer appear in the catalog. Two-layer fix: (1) catalog query excludes these brands retroactively, (2) SaveExpress scraper skips the `/fuer-kinder/` subcategory on future scrapes.
1.0.257 — 2026-04-26
Fixed
Stale brand names from SaveExpress: Products scraped before the promo-prefix stripper was in place kept the brand `*Sonderpreis*` or `+++Abena`. A startup DB migration now corrects these to `Bambino` / `Abena` respectively.
Abena "Slip 4" duplicate model groups: "Slip 4 Windeln für Erwachsene" (insenio, with old model_name) and "Slip 4 White" (SaveExpress `weiss` variant) were shown as separate products. Fixed by (1) adding `Windeln?` plural and `für Erwachsene` / `Erwachsene` to the model-noise strip list in `clean_model_name`, and (2) a DB migration that adds an alias "Slip 4 White" → "Slip 4" so all stores' Abena Slip 4 listings merge into one group.
1.0.253 — 2026-04-23
Fixed
Price-per-unit sanity guard: listings where price ÷ pack_count exceeds 10 EUR are silently dropped from the catalog (data kept in DB). Catches scraping errors like the ABDLFactory ID Expert Form Maxi that jumped from 17.99 → 377.79 EUR.
NRU scraper — Mystery Case duplicates: products with multiple option3 variants sharing the same size (e.g. Mystery Case M appearing 3×) now deduplicate to one entry per handle+size.
1.0.248 — 2026-04-23
Fixed
NRU scraper — brand "4" bug: Products whose title starts with a digit (e.g. "4 Layer 100% Microfiber Insert") were incorrectly assigned the digit as brand name. Now falls back to "NRU" when the first word is not a valid brand identifier.
NRU scraper — washable product categories: Washable nappies and inserts are now tagged `cloth` (was `aio`); plastic pants are now tagged `clothing` (was `other`).
Added
"Vaskbare" category (`cloth`): New product category for washable / cloth diapers. Keyword classifier recognises "washable", "vaskbar", "tvättbar", "waschbar", "pocket nappy", "terry towelling". Frontend shows "Cloth" (EN), "Vaskbare" (DA/NO), "Tvättbara" (SV), "Stoffwindeln" (DE).
1.0.244 — 2026-04-22
Added
abcuddle.com scraper — Shopify Italian ABDL store. Fetches all 47 products via the Shopify `products.json` collection API. Emits one `ScrapedProduct` per "Pack" type variant (skipping "2 Samples" variants). Size from `option1`, brand from Shopify vendor, model by stripping vendor prefix from product title. Capacity extracted from `body_html` product description where available.
wilulu.de scraper — Shopware 6 German ABDL store. Scrapes four adult diaper categories (tape, ABDL-print, discreet, pull-up). Each listing card is a size+pack-specific variant; price extracted from `.product-price` text, handling dual sale/original price display. Product-family landing pages (URL suffix "MAIN" with suspiciously low prices) filtered by price threshold. Pack count and capacity from product spec table on detail pages. Name parsing handles size-code+absorption labels (M10, L7), multi-word brand corrections (LFB→LittleForBig, Lil→Lil Comforts, etc.), and guards single-letter size detection against apostrophe-possessives (e.g. "Let's").
france-abdl.fr scraper — PrestaShop French ABDL store. Listing pages show one card per product (default combination); all combination IDs for other sizes are discovered from the `images[*].associatedVariants` field in the embedded `data-product` JSON. Each non-default combination is fetched individually via URL pattern `/{category}/{product_id}-{combo_id}-{slug}.html`, enabling per-size price tracking (e.g. XL often priced differently). Brand from PrestaShop product features ("Marque / Fabricant"). Trial-pack products ("Pack d'essai") and simple products (no combination) excluded.
1.0.242 — 2026-04-22
Added
insenio.de scraper — Shopware 6 German adult incontinence store. Scrapes five adult diaper and pants categories, emitting one `ScrapedProduct` per size variant. Pack price from listing card `[itemprop="price"]` (uniform across sizes per product). Pack count from `Inhalt: N Stück` on the detail page. Size options from the Shopware configurator, handling size-code+absorption-level labels (M10, XS2, etc.) and labels with inline pack counts ("S (15 Stück)"). Non-diaper products (suprima cover slips, washable shorts, PVC over-pants) filtered by name pattern. Size deduplication prevents multiple entries when a product has several absorption levels per size.
1.0.240 — 2026-04-22
Added
kiwisto.de scraper — Shopware 6 German store. Scrapes `/Windeln/` and `/Inkontinenzhilfsmittel/` across all listing pages. Each listing card is one specific size+pack variant with price from `data-price` and OOS from the "Ausverkauft" badge. Pack count and capacity are fetched from the product spec table (`Inhalt pro Packung` / `Verpackungseinheit` rows) in parallel detail-page requests, using `known_pack_counts` to cache on subsequent scrapes. Children's, sample, and bed-pad products are filtered by name pattern.
1.0.238 — 2026-04-21
Fixed
ABDL-TB-Club-Shop: prices were multiplied by pack_count (wrong) — the store's `optionPrices` in the Magento jsonConfig represent the full pack price, not a per-unit price. The scraper was incorrectly multiplying by pack_count (e.g., Attends M pack €29.98 × 28 = €839). Removed the multiplication; pack prices are now stored directly from optionPrices.
ABDL-TB-Club-Shop: cache path used listing "as low as" price — the cache optimisation that skipped re-fetching detail pages stored `listing_price` (the per-unit "as low as" display price from the category page) instead of the actual pack price. Removed the cache path so detail pages are always fetched.
ABDL-TB-Club-Shop: jsonConfig discovery — the store uses a Hyvä Alpine.js frontend; the jsonConfig is embedded in `initConfigurableOptions(id, {...})` calls in regular script tags, not in `text/x-magento-init` blocks. The existing fallback regex already handled this correctly, but the incorrect multiplication masked the correct price.
1.0.236 — 2026-04-21
Fixed
SaveExpress: additional Rearz model-first titles — added Alpaca, Astronaut, Buccaneer, and Fairytale to `_FIRST_WORD_IS_MODEL` so they are parsed as brand=Rearz rather than brand=product-name.
1.0.233 — 2026-04-21
Fixed
SaveExpress: Rearz Daydreamer brand/model swap — the listing title starts with `"Daydreamer"` (the model name), not the brand, causing the scraper to set brand=Daydreamer and model=Windel Bunt. Added a `_FIRST_WORD_IS_MODEL` lookup so these are now parsed as brand=Rearz, model=Daydreamer.
1.0.231 — 2026-04-21
Fixed
NRU: XX-Large "Case" variant no longer shown twice — added `"case"` to `_SKIP_OPTION2` so the full-case variant (which NRU labels `option2="Case"` for XX-Large, vs `"Full Case"` for other sizes) is consistently filtered out.
Diaper-Minister: radio-path products now detect global OOS — the no-click optimisation path (used for radio-type products like Rearz Daydreamers) previously exited before any OOS detection could run. A global availability check is now performed before the early return, marking all sizes as `in_stock=False` when the page shows out-of-stock before any size is selected.
Catalog: duplicate store listings per size now deduplicated — after sorting by in-stock status and price, only the best listing per store per size is kept. Prevents ghost entries (old-URL listings) from appearing alongside their replacement when both exist in the database during the transition period.
1.0.229 — 2026-04-21
Added
Citycare24.de scraper — new German incontinence store (JTL-Shop platform). Scrapes `/inkontinenzpants` (pull-up pants) and `/windeln-und-slips` (open-front slips/diapers) across all listing pages. Per-variant prices are fetched via child article URLs (`?a={child_id}`, extracted from `data-ref` attributes on the radio-button size selectors). One product is created per size at the smallest pack option. Subscription ("Spar-Abo") hidden prices are excluded; only the visible retail price is stored.
1.0.227 — 2026-04-21
Changed
Deutschland-Inkontinenz + Europe-Incontinence: detail-page OOS detection — both scrapers now fetch every product's detail page in parallel (10 workers) after the listing scrape. The `#product-availability` element is checked for "Out of stock" text. Adds ~1–2 min per store per scrape run but correctly marks e.g. iD Plastic Expert Slip Super as out-of-stock. DI also merges this fetch with the existing pack-count lookup (no double-fetching for new products).
1.0.225 — 2026-04-21
Fixed
Bib/gear accessories excluded from catalog — products with `category_hint="gear"` (bibs, pacifiers, bottles, etc.) are now filtered out of the main catalog. NRU's "Rearz Daydreamer Bib" was appearing in the Rearz Daydreamer model group due to fuzzy clustering.
Diaper-Minister: per-size OOS detection — after selecting each size option, the scraper now checks the `.product-availability` element. OOS sizes (e.g. Rearz Daydreamer Medium) are stored as `in_stock=False` instead of appearing as in-stock.
1.0.223 — 2026-04-21
Fixed
ABDL-TB-Club-Shop: pack prices stored correctly — like its sister store ABDLFactory, this store's Magento optionPrices are per-unit. The scraper now multiplies by pack_count before storing, so e.g. Attends Slip Active No.10 is stored as €59.64/28 instead of €2.13/28 (which gave a nonsensical €0.076/pc).
1.0.221 — 2026-04-20
Changed
AB-Versand scraper disabled — the store blocks price comparison sites and consistently returns 0 products. The "not included" notice in the store menu is retained. Saves ~21s per scrape run.
1.0.219 — 2026-04-19
Fixed
Dalille: bundles omitted from catalog — the `doublepacks` collection is no longer scraped, and a broader title filter now skips bundle-type products (`dobbeltpakke`, `bundle`, `mix-pak`, `sæt`) from the main diapers collection. Bundle prices distort per-unit comparisons.
InControl BeDry brand mapping — products listed as "BeDry" or "Be Dry" now resolve to the InControl brand.
SyntaxWarning in med24.py — invalid escape sequence `\d` in a docstring comment fixed.
1.0.218 — 2026-04-20
Fixed
Catch-up scrape on container restart — if the container starts more than 20 hours after the last scrape (e.g. after a restart that missed the 02:00 UTC window), a catch-up scrape now runs automatically in the background on startup.
1.0.217 — 2026-04-20
Fixed
Deutschland-Inkontinenz: missing pull-up categories — added `/en/40-classic-pull-ups` and `/en/41-easy-pull-ups` to the scraped category list, and updated the existing slugs to their current canonical forms (`/en/4-all-in-one-briefs`, `/en/5-belted-briefs`). The old slugs redirected correctly but this makes intent explicit.
Note: out-of-stock products (e.g. Id Plastic Expert Slip Super) are hidden from DI listing pages — the scheduler's missing-product detection will mark them OOS automatically on the next scrape run.
1.0.216 — 2026-04-20
Changed
Footer note now clarifies prices exclude shipping — all 5 languages updated (EN: "excl. shipping", DA: "ekskl. fragt", NO: "ekskl. frakt", SV: "exkl. frakt", DE: "zzgl. Versand").
1.0.215 — 2026-04-20
Fixed
NRU store: all products attributed to brand "NRU" — NRU's Shopify store reports `vendor="NRU"` for every product regardless of the actual brand. The scraper now extracts the brand from the first word of the title (e.g. "NorthShore MEGAMAX Airlock" → brand "NorthShore", model "MEGAMAX Airlock"). NRU's own products still start with "NRU" so they remain correctly attributed.
Added `"northshore"` and `"incontrol"` to `_BRAND_CANONICAL` for correct display names ("NorthShore", "InControl").
1.0.214 — 2026-04-19
Fixed
NorthShore MegaMax Tie-Dye / Tie Dye not grouping — two-part fix: added `Tie-Dye` (hyphenated) to `_CAMEL_NORM` so it normalises to `Tie Dye` before comparison; added a space-comma collapse step in `clean_model_name` so German-store titles like `"MEGAMAX Windel ,Tie-Dye"` no longer lose their colour variant to the trailing comma-strip (same fix also correctly groups `MEGAMAX Windel ,blau/schwarz/weiss`).
Changed
What's New modal: "Full changelog (English)" footer link — a small link at the bottom of the What's New modal opens the developer CHANGELOG in a new tab, with translated label in all 5 languages.
1.0.213 — 2026-04-19
Added
What's New modal — replaces the plain "Latest changes" link in the header. Opens an inline modal with user-friendly change summaries translated into all 5 languages (EN/DA/NO/SV/DE). A "NEW" badge appears when there are entries newer than the last time the modal was dismissed (tracked in localStorage). Data is driven by `app/static/whats_new.json`.
1.0.211 — 2026-04-19
Fixed
Diaper-Minister: LNGU shown as "LNGU / LNGU" — when a DM slug ends with a trailing dash (e.g. `lngu-`), `_parse_dm_slug` was producing `slug_model = "Lngu"` which equals the brand, causing the mismatch-override logic to replace the correct listing-derived model "Rainbow Pastel" with "Lngu". Empty slug segments are now filtered out, and the override is skipped when `slug_model == slug_brand`.
Diaper-Minister: Teddy Bums missing brand — listing title "Teddy Bums" doesn't include the brand "Lil Comforts". Added `teddy-bums` to `_DM_SLUG_BRAND_MAP` and "lil comforts" to `_DM_MULTIWORD_BRANDS` so the correct brand/model are used regardless of title.
1.0.210 — 2026-04-19
Fixed
Cloudrys: third-party brand pack count always None — "Inhalt pro Packung 15 Stück" in the product body HTML is now parsed as a fallback when the title doesn't contain a "N Stück" prefix (e.g. Crinklz, Rearz products on Cloudrys.de).
1.0.209 — 2026-04-19
Fixed
Price graph: stores without pack count shown at pack price — when `price_per_unit_eur` is null (no pack count known), the chart was falling back to `price_eur` (the total pack price), making those series appear ~10–15× higher than per-unit lines. Chart now shows a gap (null) for data points with no per-unit price.
1.0.208 — 2026-04-19
Fixed
Crinklz models fragmented across 28 buckets — added `_crinklz_canonical()` (mirrors the BetterDry approach) that collapses any model_key containing "aquanaut/astronaut/buccaneer/fairy/classic" to the five official product lines, with everything else falling back to "Original". Applied at catalog render time so no re-scrape is needed.
"Nat" (Danish/Norwegian for night) not translated — added `('nat', 'Night')` to `_LOCALE_TRANS` in `clean_model_name` so "Windelhose Nat bunt" normalises to "Windelhose Night bunt" and collapses with "Windelhose Nacht bunt".
1.0.207 — 2026-04-19
Fixed
Model price graph: values always shown in EUR — `loadPriceHistoryChart` now applies the active exchange rate and currency symbol. Also removed hardcoded "EUR" from the chart title i18n strings.
ABDL-TB-Club-Shop: Crinklz price read as per-piece "as low as" price — `_find_magento_json_config` only searched for `"jsonConfig":` keys; this store embeds config as `initConfigurableOptions('id', {...})`. Extended fallback to also match this pattern so Pack variant prices (e.g. €22.84 for M/15pc) are used instead of the per-piece minimum (€2.62).
Crinklz models not grouped — added model aliases so SaveExpress "Windelhose Night bunt" and "Aquanaut Windelhose Night [bunt]", Voksenbleer bare "Crinklz", and ABDLFactory "Adult Diapers with Print" all resolve to the correct canonical model names (Original / Aquanaut).
1.0.206 — 2026-04-19
Fixed
Price graph: values always shown in EUR regardless of selected currency — chart Y-axis and tooltip now multiply EUR values by the active exchange rate and display the correct currency symbol (kr, € etc.).
1.0.205 — 2026-04-19
Fixed
Cutieplusu: products not categorised as AIO — added `("cutieplusu", "", "aio")` brand override so all Cutieplusu products default to the AIO category.
1.0.204 — 2026-04-19
Fixed
SaveExpress: "Aquanaut" parsed as brand instead of "Crinklz" — SaveExpress lists these products as "Aquanaut Crinklz Windelhose …", putting the model-line name first. Added "Aquanaut" → "Crinklz" to `_BRAND_CORRECTIONS` and added a word-swap step so the model becomes "Aquanaut Windelhose Nacht" rather than "Crinklz Windelhose Nacht".
1.0.203 — 2026-04-19
Fixed
SaveExpress: size missing when listing title is truncated — same truncation issue as pack count: "Medium" gets cut off before the `...`. Added fallback to `parse_size(href)` when `_parse_se_name` returns `size=None`.
1.0.202 — 2026-04-19
Added
SaveExpress: extract capacity_ml from listing description — "Saugleistung etwa 3500ml" / "Saugleistung : ca. 5000ml" patterns in `div.product--description` are now parsed into `capacity_ml`.
1.0.201 — 2026-04-19
Fixed
SaveExpress: pack count always None after Sonderpreis fix — listing titles are truncated with "…" so "8er-Packung" never appears in the scraped text; the count is in the URL slug instead (e.g. `8er-packung`). Added fallback: if `parse_pack_count(name)` returns None, parse the product URL. Also added filter: skip `einzelstueck` (single-piece) listings which are not comparable to multi-pack prices.
1.0.200 — 2026-04-19
Fixed
Europe-Incontinence: rectangular pads and anatomical protection still 0% pack count — the listing subtitle format uses 2 adjective words before the unit: "30 permeable rectangular pads", "14 anatomical protections". The regex only allowed 1 optional word (`(?:\w+\s+)?`). Changed to `(?:\w+\s+){0,2}` and added "protections?" to the unit word list.
1.0.199 — 2026-04-19
Fixed
Invizi: "Twin Pack" and "3 Pack" products have no pack count — these reusable incontinence accessories have a colour/size in `option1` and carry the pack quantity solely in the product title (e.g. "Twin Pack", "3 Pack"). Added `_parse_pack_from_title()` as a fallback; "Twin Pack" → 2, "N Pack" → N.
1.0.196 — 2026-04-19
Fixed
Abena Privat: empty model names for tape diapers — "Tapeble" was missing from `_TYPE_PREFIX_RE`, so it was not stripped early enough. The downstream `clean_model_name()` then removed it and left an empty string. Added "Tapeble" to the prefix list.
Abena Privat: missing sizes for M0–M4, S1–S4, L-XL, M-L — `_ABENA_SIZE_RE` did not cover these codes. Rewrote the regex to match all Abena compound and numeric size codes; extended `_ABENA_SIZE_MAP` accordingly.
SaveExpress: "Sonderpreis" parsed as brand name — promotional titles like "*Sonderpreis* Abena…" had the first word parsed as brand. Added `_PROMO_PREFIX_RE` to strip leading `*`, `+`, `!`, and "Sonderpreis" tokens before brand/model extraction.
Europe-Incontinence: pack count always None — pack count lives in `h3.product-subtitle` (e.g. "14 absorbent briefs – measurement..."), not `p.product-description`. Added subtitle extraction and extended `_PACK_RE` to allow an adjective between the count and the unit word.
Voksenbleer: BetterDry/Crinklz products have no pack count — the JSON API never returns pack count; the scraper now falls back to the DB cache and then fetches the HTML detail page to extract "N stk" from the product page text.
Voksenbleer: only last product appended — `products.append()` was outdented outside the `for` loop, so only the final product was ever added. Fixed indentation.
Voksenbleer: L-XL/M-L remained in model name — `_ABENA_CODE_RE` required a trailing digit so bare "L-XL" was not stripped. Updated regex to make the digit optional.
ABUniverse: NorthShore MegaMax White pack count None — `_PACK_COUNT_RE` only matched the word "Diaper". NorthShore variants use "Briefs". Extended the regex to also match Briefs, Pieces, Pcs, Items, Units.
ABDL-TB-Club-Shop: 0% pack count coverage — the pack count regex in `_fetch_detail()` did not cover Dutch terms ("stuks", "luiers"). Extended the regex and added a full-page fallback search when the attribute table yields nothing.
1.0.194 — 2026-04-19
Fixed
MaxDiaper: brand from breadcrumb — when MaxDiaper uses a brand-named category as the breadcrumb (e.g. "ABU"), the scraper now uses it as the product brand. Fixes "Cloth-Backed Cushies" appearing under brand "Cloth-Backed" instead of ABU.
MaxDiaper: URL-targeted pack count reset — added `/api/admin/reset-pack-count-by-url` endpoint (POST with `{"urls": [...]}`) to null out stale cached pack counts for specific product URLs.
1.0.192 — 2026-04-19
Fixed
MaxDiaper: pack_count=5 false-positive from star ratings — A stale regex matched "5 av 5 stjärnor" (rating text) and stored 5 as the pack count for clothing and accessory products with no actual pack. Added `_PACK_I_PAKET_RE` to capture the `N [noun] i ett paket` pattern (e.g. "28 förstärkningsskydd i ett paket") that the main regex missed. Added `/api/admin/reset-pack-count` endpoint to null out bad cached values so the next scrape re-fetches the correct counts.
1.0.191 — 2026-04-19
Fixed
Cloudrys: trailing `(N Stück)` suffix also skipped — products like "Cloudrys Superheros Windeln mit Folie (2 Stück)" had the pack count in a parenthetical suffix rather than a leading prefix, so `pack_count` parsed as `None` and the product got the Cloudrys-brand default of 10 instead of being skipped. Extended `_STUECK_RE` to match both `^N Stück` prefix and `(N Stück)$` suffix forms.
1.0.188 — 2026-04-19
Fixed
Cloudrys: pack_count=10 for Cloudrys-branded products — Cloudrys' own products always ship in packs of 10, but this was not stated anywhere in the title or description. Third-party brands (Crinklz, Rearz, etc.) keep `null` and source their pack count from other scrapers.
1.0.186 — 2026-04-19
Fixed
Cloudrys: skip 2-pack trial packs — products with a parsed pack_count < 10 (e.g. "2 Stück" titles) are skipped. Only full packages are included in the catalog.
Catalog: null pack_count correctly included — `NULL != 1` evaluates to NULL in SQL (falsy), so null-count rows were wrongly excluded. Filter is now `pack_count != 1 OR pack_count IS NULL` so products with unknown pack size are shown as originally intended.
1.0.183 — 2026-04-19
Added
Per-size price chart — 📈 icon on each size row in the product view opens a modal with 90-day price history for that size only, one line per store. Translated into all 5 languages.
`/om` page respects selected language — the footer link now passes `?lang=da/en/…` so the About page is served in the active UI language rather than just the browser language.
1.0.181 — 2026-04-19
Changed
"En del af ageplay.dk" header subtitle is now translated for each language (EN: "Part of", NO/SV: "En del av", DE: "Teil von").
Footer "About" link text is translated into all 5 languages via i18n.
`/om` page fully translated into Danish, Norwegian, Swedish, German, and English — language is auto-detected from the `Accept-Language` header.
1.0.179 — 2026-04-19
Added
"En del af ageplay.dk" header subtitle — small link below the PaddingPrices logo pointing to ageplay.dk.
`/om` — About page — new static page describing PaddingPrices and its connection to ageplay.dk. Linked from the footer.
1.0.177 — 2026-04-19
Fixed
Diaper-Minister: trust URL slug over listing text for model name — when the listing card's title doesn't match the URL slug's model portion (e.g. slug `116-33474-abu-little-pawz` but listing card says "ABU AlphaGatorz"), the scraper now derives brand+model from the slug instead. Fixes Little Pawz listings being mis-attributed to AlphaGatorz because DM's listing card title for that URL genuinely says "ABU AlphaGatorz".
CuddleKingdom: handle-title mismatch guard (stronger) — replaced the handle-brand check with a handle/title model-name comparison. Catches renamed-within-brand products where handle says `abu-cushies-2-tape-l` but current title says "ABU AlphaGatorz L". Both renamed-to-different-brand and renamed-within-brand products are now skipped.
Scheduler: purge stale out-of-stock products — products that were already out-of-stock last run AND are still missing this run are now hard-deleted (row + all price history). Without this, scraper-skipped pollution would remain visible as grey/OOS listings indefinitely. Safety guard: if a scrape returns 0 items, the missing-products pass is skipped entirely so one bad scrape can't nuke a store.
1.0.175 — 2026-04-19
Fixed
Catalog: Los / kopie-van- URL filter — the `/api/catalog` query now excludes products whose URL ends with `-los` or contains `/products/kopie-van-`. This immediately hides stale CuddleKingdom records that were ingested before the scraper learned to skip them, without requiring a DB cleanup.
CuddleKingdom: skip "kopie-van-" handles — products whose Shopify handle starts with `kopie-van-` (Shopify-generated duplicates) are now skipped by the scraper, preventing them from re-entering the DB.
CuddleKingdom: handle-brand mismatch guard — products where the handle's embedded brand name differs from the current title's brand (e.g. handle `abena-abriform-…` with title "ABU AlphaGatorz …") are skipped. Prevents renamed Shopify products with stale handles from being mis-attributed to the wrong brand/model.
1.0.173 — 2026-04-19
Fixed
CuddleKingdom "Los" skip — updated regex from `\s+Los$` to `[\s\-]Los$` so titles like "ABU AlphaGatorz M-Los" (hyphenated) are also skipped. Added a second check on the Shopify product handle (handles ending in `-los` are always skipped regardless of title format).
Diaper-Minister cache URL mismatch — added a slug-vs-model guard: if the cached variant URL's product slug doesn't contain words from the current listing's model name, the cache is not used and the detail page is re-fetched. This prevents stale cross-product associations when DM reuses old product URLs for different products.
1.0.171 — 2026-04-19
Added
Abena Privat capacity scraping — the `abenaprivat.py` scraper now reads "Absorptionsevne" from the product detail page attribute table and stores it as `capacity_ml`. Useful for products not stocked by Diaper-Minister (the other primary capacity data source). Detail fetches are skipped when both size and capacity are already cached.
Fixed
Hash navigation slug match — model lookup in URL routing now also does a slug-based fallback (`strip non-alphanumeric` on both sides) so that URLs like `#/ABU/Alphagatorz` correctly navigate to a model stored as "AlphaGatorz" or "Alpha Gatorz" regardless of spacing or capitalisation differences between stores.
1.0.168 — 2026-04-19
Fixed
Test suite — updated `test_clean_model_name` expectations to match the intentional Abena code behaviour introduced in v1.0.157: `M4` → `4` (letter stripped, absorbency digit kept). Tests were still expecting the old full-strip behaviour.
Changed
Store country assignments — corrected after quality check:
- Diaper-Minister: BE → 🇫🇷 FR (French company)
- Europe-Incontinence: BE → 🇫🇷 FR (French company)
- ABDLFactory: DE → 🇳🇱 NL (Netherlands-based)
- ABUniverse: US → 🇳🇱 NL (ships from Netherlands warehouse)
- LittleForBig: US → 🇩🇪 DE (EU orders from Germany warehouse since Nov 2023)
- Belgium removed from country filter (no stores remain)
1.0.165 — 2026-04-19
1.0.165 — 2026-04-19
Added
Country-level store filter — the store picker now shows shortcut buttons for each country (🇩🇰 DK, 🇩🇪 DE, 🇳🇱 NL, …). Clicking a country button enables or disables all stores from that country in one click. Individual store checkboxes remain below, grouped by country. Button styling reflects current state: filled border = all on, dim = partial, plain = all off.
1.0.163 — 2026-04-19
Fixed
Hash navigation case-insensitive — brand and model lookup in URL routing (`#/ABU/Alphagatorz`) now uses case-insensitive comparison. Previously a capitalisation mismatch (e.g. `"Alphagatorz"` vs the stored `"AlphaGatorz"`) caused navigation to fall back to the brand models list instead of opening the product detail page.
1.0.158 — 2026-04-18
Fixed
ABU brand display — ABDLFactory stores ABU brand as "Abu" (`.title()` on "ABU"). Added `"abu"` to the canonical map so it always displays as "ABU" regardless of which store's product is indexed first.
Cloth-Backed mis-classification — removed `"cloth-backed"` from `_BRAND_CANONICAL`. "Cloth-Backed" is a generic incontinence material descriptor; having it map to ABU caused unrelated incontinence products (e.g. from Europe-Incontinence) to appear under the ABU brand.
1.0.157 — 2026-04-18
Fixed
Abena M1 vs M3 mixing — `clean_model_name()` now preserves the absorbency level digit from Abena size codes (M3→3, L3→3, XL2→2) instead of stripping the entire code. M1 and M3 now produce distinct model keys and appear as separate products.
Ugleapo size extraction — Abena codes like "M3", "L3", "XL1" are now correctly parsed: the letter gives the size (M, L, XL) and the digit gives the absorbency level.
NRU pack count — NRU "Pack" variants are hardcoded as 10 pieces per pack (not extractable from body HTML or variant options). 188 of 227 nappies now have a pack count and will show price-per-unit.
1.0.155 — 2026-04-18
Added
Scraper category hints — scrapers can now provide a `category_hint` that feeds directly into product classification, taking precedence over keyword-based auto-detection but below admin overrides. Stored as a new `category_hint` column on `products`.
NRU categorization — all 642 NRU products are now correctly categorized using Shopify `product_type`: nappies → aio, clothing → clothing, pacifiers/bottles/bibs → gear, boosters → booster.
1.0.154 — 2026-04-17
Added
Ugleapo scraper — new scraper for ugleapo.dk/kategori/inkontinens/ (Danish WooCommerce store). 121 products, prices in DKK.
NRU expanded collections — now scrapes accessories, clothing, and washables-and-plastics in addition to nappies (642 products total, up from 204).
Fixed
NRU currency — EU store (eu.nru.co.uk) was incorrectly tagged as GBP; corrected to EUR.
1.0.153 — 2026-04-17
Added
User accounts — optional login via magic link (email, no password). 30-day session cookie (`pp_session`, httpOnly).
🔑 Account button in header — opens a modal with email input; sends login link via Brevo. Shows email + actions when logged in.
Cloud sync — favorites and notes sync to server when logged in; changes are written to both localStorage and server in real time. Language and currency preferences also synced.
Import — on first login, a "Sync local data to account" button pushes existing localStorage favorites and notes to the server.
New models: `User`, `LoginToken`, `UserSession`, `UserFavorite`, `UserNote`, `UserPreference`.
New endpoints: `POST /api/auth/login`, `GET /api/auth/verify/{token}`, `POST /api/auth/logout`, `GET/PUT /api/me/preferences`, `GET/POST/DELETE /api/me/favorites`, `POST /api/me/favorites/bulk`, `GET/PUT/DELETE /api/me/notes`.
`/api/me` now returns `logged_in`, `email`, `currency`, `lang` alongside `is_admin`.
1.0.150 — 2026-04-17
Fixed
LittleForBig brand name — DB migration corrects existing products with brand `LFB` or `Little` (scrape artifacts from old name parsing) to `LittleForBig`. Scraper already emits the correct name; this cleans up historical records.
1.0.148 — 2026-04-17
Added
Store overview (🏪) — new view lists all stores with brand/product counts and average price-per-unit. Clicking a store card switches to the flat list pre-filtered by that store.
Side-by-side comparison — ⊕ button on each model card adds up to 3 models to a compare bar (sticky bottom). Clicking "Compare" opens a modal showing best price-per-unit per size across selected models, highlighting the cheapest.
Price trends (📉) — new view calls `GET /api/price-trends?days=7` to surface the biggest price-per-unit drops over the last 7 days. Clicking a trend row navigates to the product page.
`GET /api/price-trends` endpoint — returns up to 20 products with the largest percentage ppu drop in the given window, deduplicated to one entry per brand+model.
Hash routing for `/stores` and `/trends` so views survive page refresh and browser back/forward.
Changed
Flat list view (📋): respects `storeFilter` set by the store overview so clicking a store card shows only that store's products.
Brand-sort dropdown now also hidden in Stores and Trends views (irrelevant there).
1.0.142 — 2026-04-17
Added
Price alerts — 🔔 button on each size block in the product view lets visitors set a price alert. Enter email + target price/unit; an email is sent via Brevo when the price drops to or below the target. Cooldown of 7 days prevents repeat emails. Unsubscribe link in every email. Pre-fills email from previous alert. i18n in all 5 languages. Admin can view and delete alerts at `/api/admin/alerts`. Alerts are checked automatically after every scrape run.
1.0.139 — 2026-04-17
Changed
LittleForBig — scraper now uses the EU store (`eu.littleforbig.com`). Prices scraped directly in EUR instead of USD, removing the need for currency conversion.
1.0.138 — 2026-04-17
Changed
Docs: CHANGELOG updated for 1.0.134–1.0.137; README updated with language URL path feature.
1.0.137 — 2026-04-17
Added
Language URL paths — `/da`, `/no`, `/sv`, `/de`, `/en` load the site in the respective language. URL is updated automatically when the user switches language via the dropdown.
1.0.136 — 2026-04-17
Added
Umami analytics — self-hosted, privacy-friendly, cookie-free tracking via `pricestats.diaper.dk`. Script activated in `<head>`.
1.0.135 — 2026-04-17
Changed
Umami host changed from `stats.prices.diaper.dk` to `pricestats.diaper.dk`.
1.0.134 — 2026-04-17
Added
Favicon — emoji SVG favicon (🐻), no extra file needed.
SEO meta tags — `<meta name="description">` and Open Graph tags (`og:title`, `og:description`, `og:url`, `og:type`).
Umami Docker setup — `umami` and `umami-db` services added to `docker-compose.yml`; tracking script placeholder added to `index.html`.
Feedback records page URL — `page_url` column stores the page the user was on when submitting feedback; shown as a clickable link in the admin panel.
1.0.133 — 2026-04-17
Added
Feedback — 💬 Feedback button in the footer opens a modal where visitors can send a message (and optional email for a reply). Rate-limited to 3 submissions per IP per hour. Messages stored in the database. Admin panel shows all feedback with unread badge, mark-read, and delete actions. Full i18n in all 5 languages (EN/DA/NO/SV/DE).
1.0.132 — 2026-04-17
Changed
Shopping cart redesigned — cart now operates at model+size level (not store level). 🛒 button moves to the size block header in the product view. Cart modal shows two sections: "Cheapest per item" (optimal mix across stores) and "Cheapest single store" (best store to buy everything from in one go), with totals for both. Items not in stock at a given store are marked as unavailable.
1.0.131 — 2026-04-17
Added
Shopping cart — 🛒 button on each listing row in the product view adds that store/size to a local cart. Cart icon in the header shows item count badge. Cart modal shows all items with pack price, price/unit, store, and "Buy →" links. Estimated total and "Clear cart" at the bottom. Cart stored in `pp_cart` (localStorage).
Notes redesigned — 📝 button in the product hero (next to ❤️) toggles a collapsible note panel that slides open below the hero. Button glows yellow when a note exists. Saves on each keystroke with a brief "Saved" flash.
1.0.130 — 2026-04-17
Added
Personal notes — 📝 editable text area on every product page for private notes (e.g. "tried this, too thick" or "order when below €0.20/pc"). Saved in `pp_notes` (localStorage). A 📝 dot appears on model cards that have a note.
Local data notice — small text line below the category filters in all 5 languages: favorites and notes are device-local for now; user profiles with cross-device sync are on the roadmap.
1.0.129 — 2026-04-17
Added
Favorites list — heart button (🤍/❤️) on every model card and product page; click to toggle. Favorites stored in `pp_favorites` (localStorage, no account required).
Favorites filter button — ❤️ toolbar button filters all views (brands, models, flat list) to show only favorited models. Active state is visually highlighted in pink.
ROADMAP.md — feature roadmap file with tiered plans (client-side, email subscriptions, full accounts). Includes personal notes, shopping cart, price drop notifications, user accounts, and shipping prices.
1.0.128 — 2026-04-17
Removed
Tor sidecar removed from docker-compose.yml — AB-Versand blocks all known Tor exit-node IPs (Status 470), so the `tor` service and `TOR_PROXY` env var served no purpose.
1.0.127 — 2026-04-17
Added
AB-Versand shown as blocked in store picker — a grayed-out 🚫 entry at the bottom of the store list explains in all 5 languages that AB-Versand is not included because they block price comparison sites.
1.0.126 — 2026-04-17
Changed
AB-Versand: ScraperAPI residential proxy — Tor exit-node IPs are explicitly blocked by AB-Versand (Status 470). Scraper now uses ScraperAPI as HTTP proxy (`SCRAPERAPI_KEY` env var). Images/CSS/fonts blocked in Playwright to conserve credits. Falls back to Tor if key is unset. Phase 2 (detail pages via httpx) also routes through ScraperAPI.
1.0.125 — 2026-04-17
Fixed
AB-Versand: WAF challenge not resolving — scraper was using `wait_until="domcontentloaded"` which returned immediately with the challenge page before Playwright's JS engine could solve the SHA-256 PoW and follow the redirect. Changed to `wait_until="networkidle"` (90s timeout) so Playwright waits for the full challenge → redirect → product page cycle. Added diagnostic logging (final URL, page title, HTML snippet) to make future debugging easier.
1.0.124 — 2026-04-15
Added
`/robots.txt` — disallows `/admin`, `/api/`, `/login`, `/logout` from search engine indexing; allows everything else (catalog, changelog).
1.0.123 — 2026-04-15
Added
Tor proxy for AB-Versand — added a `tor` sidecar service (`osminogin/tor-simple`) to `docker-compose.yml`. AB-Versand's server IP block is bypassed by routing all AB-Versand requests through a Tor SOCKS5 proxy (`socks5://tor:9050`, configurable via `TOR_PROXY` env var).
AB-Versand scraper: Playwright + Tor — rewrote the scraper to use Playwright (Chromium) via the Tor proxy for listing pages. Playwright executes the SHA-256 PoW WAF challenge client-side and follows the redirect to the real product page. WAF session cookies are then passed to httpx (also via Tor) for faster detail-page fetching.
`socksio` added to `requirements.txt` — required for httpx SOCKS5 proxy support.
1.0.121 — 2026-04-15
Fixed
Apopro scraper: 0 products — Apopro renders prices as `"135,00kr."` (number and unit concatenated). `parse_price()` kept the trailing `.` from `kr.`, producing `"135,00."` which was misread as European thousands format → `13500.0`. Every product exceeded `MAX_PRICE_DKK=5000` and was silently dropped. Fixed by extracting just the numeric part before `kr` with `_PRICE_KR_RE` before calling `parse_price`. Also simplified price extraction from three fallback strategies to one regex pass on the card text.
1.0.119 — 2026-04-14
Added
Multilingual search synonyms — searching in Danish/Norwegian/Swedish now works for common product terms. `nat`/`natt` → night products, `bukse` → pants/pull-up, `svømme` → swim, `bind` → pad/liner/shaped, `dag` → day/daytime, `lille`/`liten` → small. Synonyms expand to the English terms stored in the database; direct substring match still takes priority.
1.0.117 — 2026-04-14
Fixed
ID Belt → AIO: Products with brand "Id" and model containing "belt" are now correctly categorised as Alt-i-et (belted/taped brief).
LNGU → AIO: All LNGU products now default to Alt-i-et.
Land of Genie brand truncation: ABUniverse scraper uses `title.split()[0]` as brand for passthrough vendors, so "Land of Genie" products arrived with brand "Land". Added `("land", "of genie", "aio")` rule that matches when the full product title contains "of genie".
1.0.116 — 2026-04-14
Fixed
ABUniverse Thrust Vector vendor: Shopify `vendor` field for several ABDL brands (BetterDry, Crinklz, Land of Genie) is set to "Thrust Vector" (the US distributor). Products were appearing under a "Thrust Vector" brand group instead of their real brand. Fixed by using `title.split()[0]` as brand whenever the vendor is a known passthrough ("Thrust Vector", "ABUniverse", "AB Universe").
ABU sub-brand grouping: ABUniverse products sold under ABU sub-brand names (PeekaABU, LittlePawz, AlphaGatorz, DinoRawrZ, BunnyHopps, TinyTails) are now all categorised as AIO by brand default, and their first-word brand is added to `_BRAND_CANONICAL` so they group under the ABU umbrella.
Generic ABUniverse passthrough brands ("Simple", "Super", "Little", "Oops") now default to AIO.
1.0.115 — 2026-04-14
Fixed
Goodnites → Pullup: added brand default so Goodnites products no longer fall through to Andet.
Abena San → Bind: Abena San 1–11 anatomical pads now correctly classify as Bind instead of Alt-i-et.
Abena Let / Let Anatomic → Bind: explicit brand default added.
BetterDry "Day Level 7" → AIO: `_RE_PAD`'s `level\s*[3-9]` pattern was matching before the BetterDry brand rule. Fixed by moving `_STATIC_DEFAULTS` lookup ahead of all keyword regex checks.
Abena Abri-Wing → AIO: explicit brand/model default added.
Cloudrys, Land of Genie → AIO: added brand defaults.
1.0.107 — 2026-04-13
Fixed
AB-Versand: IP-blocked — `ab-versand.de` blocks the server's IP at TCP level (`ConnectTimeout`). Added early-abort after 2 consecutive failures (saves ~10 min of futile waiting). Scraper is effectively disabled until a rotating proxy is configured. Fixed docstring SyntaxWarning in med24.py (`\d` → raw string notation).
Known issues
AB-Versand requires a residential/rotating proxy to scrape from a data-center IP.
1.0.103 — 2026-04-13
Fixed
Med24 scraper: 0 products — product URL regex `\d{4,}` required a 4+ digit numeric ID, but Med24 uses purely descriptive slugs ending in `-{pack_count}-stk` (e.g. `abena-slip-l1-26-stk`). Regex replaced with `-\d+-stk` pattern. Also added `/voksenbleer` (adult diapers) subcategory to the scrape list.
AB-Versand scraper: 0 products — WAF challenge wait increased from 4 s to 10 s + `networkidle` wait added so the real product page finishes loading before HTML is captured. Added diagnostic logging (card count before/after scroll, WAF detection check, page snippet when 0 cards found).
1.0.102 — 2026-04-13
Fixed
Admin category override not applied for aliased brands: `cat_overrides` and `hidden_set` were built using `r.brand.lower()` (e.g. `"betterdry"`) but looked up with `brand_key` from `_normalise_brand()` (e.g. `"better"`). The keys never matched for any brand with a canonical alias (BetterDry, ABU, LFB, etc.). Fixed by using `_normalise_brand(r.brand)[0]` when building both dicts so overrides now apply correctly.
1.0.101 — 2026-04-12
Fixed
MaxDiaper BetterDry sizes in cache paths: when `pack_count` was already stored in the DB, the scraper used the cached `size` from `known_sizes` instead of the freshly-extracted `bd_size` from the product title. Both cache paths (sz-variant and single-product) now prefer `bd_size`.
MaxDiaper BetterDry size regex: `_BD_SZ_RE` had a `\s*$` end anchor that caused it to fail when the title had trailing text after the size+level code (e.g. `"Day M10 Medium"`, `"15 st L10"`). Anchor removed; pattern now matches anywhere in the remainder.
MaxDiaper BetterDry always uses title size: each MaxDiaper BetterDry URL is one size variant (M10, L10, M7, L7 are separate listings). The scraper now always takes `bd_size` from the title rather than falling back to the detail-page `<select>`, which would otherwise create duplicate M+L records from a single URL.
Added
Out-of-stock toggle checkbox in the filter bar (red chip, next to category filters). Hides/shows out-of-stock listings across brand grid, list view, and product detail. Preference persisted in `localStorage` (`pp_show_oos`).
Admin: clean up zero-price records — new 🧹 card in `/admin` dashboard with `DELETE /api/admin/cleanup/zero-prices` endpoint. Removes all `price_eur = 0` sentinel records that pollute price history graphs.
Fixed
Price = 0 for out-of-stock products: price history graph now filters out `price_eur = 0` records. List view and product detail show `—` instead of `0` for out-of-stock sentinels.
MaxDiaper `"Better Dry"` brand not grouped: `_normalise_brand` only had `"betterdry"` and `"better"` — `"better dry"` (with space, as MaxDiaper writes it) fell through as an unknown brand, so `_betterdry_canonical()` was never called. Added `"better dry"` to `_BRAND_CANONICAL`.
Changed
Out-of-stock visibility: catalog previously always filtered `in_stock = 1`. Now only Dalille's out-of-stock products are hidden (their "Kan ikke bestilles" variants). All other stores show out-of-stock listings with a badge. In-stock listings sort first within each size; best price/unit shown is always from in-stock stock.
BetterDry 10 and 7 Day → AIO category default.
SENI → AIO category default. Products matching `tücher`/`tuecher` (wipes) → Other, taking precedence over the brand default.
Fixed
Diaper-Minister `CATEGORY_URLS` missing comma: Python silently concatenated `"/en/65-other-protection-products"` and `"/en/56-wipes-and-care-creams"` into one broken URL — only 2 of 4 categories were scraped, marking ~100 products as out-of-stock incorrectly.
1.0.90 — 2026-04-12
Performance
Diaper-Minister scrape time: 503s → 80s (6-7× faster)
- `wait_until="domcontentloaded"` instead of full page load: eliminates 60-90s wait per product
- Selector-find: replaced 4× broad Playwright DOM scans with BeautifulSoup (already-parsed HTML) + single targeted `page.query_selector()`: ~950ms → <200ms per product
- Radio-button products: removed click+wait+inner_text loop entirely — price always fell back to `base_price` anyway (DM's price selector doesn't match radio-variant pages). Sizes read directly from label scan: ~630ms saved per size variant
- JS click (`page.evaluate`) instead of `inp.click()`: avoids Playwright's 30s network-settle wait after each radio click
1.0.82 — 2026-04-12
Added
BetterDry.com scraper — official brand store (Shopify/EUR). Scrapes all three product lines (10 / 7 Day / Pull-on) with size and pack count from Shopify variant options.
Fixed
Admin logs always empty: JavaScript was looking for `d.lines` (array) but API returns `d.log` (string). Fixed JS to use `d.log`.
1.0.80 — 2026-04-12
Fixed
BetterDry "Day" = 7 Day, not 10: `_betterdry_canonical()` now correctly maps model keys containing "Day" (without a number) to `7 Day`. Bare/empty model keys (stores listing just "BetterDry L") still map to `10`.
1.0.78 — 2026-04-12
Fixed
BetterDry cross-store model grouping: all stores (Dalille, Voksenbleer, Diaper-Minister, MaxDiaper, CuddleKingdom) now map BetterDry products to the three canonical product line names — `10`, `7 Day`, `Pull-on` — regardless of how each store names them. Logic lives centrally in `main.py` and runs after brand-prefix stripping.
1.0.77 — 2026-04-12
Changed
BetterDry model naming now follows BetterDry's own product line names: absorbency 10 → `10`, absorbency 7 → `7 Day`, pull-on variants → `Pull-on`
1.0.76 — 2026-04-12
Fixed
MaxDiaper BetterDry "Up and go": "Better Dry" (two-word brand as written by MaxDiaper) now recognised; `M8 Medium` suffix pattern handled (optional spelled-out size word after the absorbency number); model name correctly becomes "Up and go" with size M
CuddleKingdom BetterDry: products listed as "Betterdry L" / "Betterdry M" (no model name) now produce model "Day" matching the standard BetterDry Day variant
Category: BetterDry "Up and go" auto-classifies as pullup
1.0.73 — 2026-04-12
Fixed
MaxDiaper / BetterDry size + model parsing: BetterDry products on MaxDiaper encode size and absorbency level in the title (e.g. "Day M10", "Day L7"). The scraper now extracts the size letter (M/L) and uses the absorbency level to set the correct model name — level 10 → "Day", level 7 → "Day Light" — so variants group correctly across stores.
1.0.68 — 2026-04-12
Added
Store picker: Select all checkbox at the top of the store dropdown — uncheck to hide all stores at once; individual checkboxes keep it in sync
1.0.66 — 2026-04-12
Added
Category filter now remembered between visits (saved to `localStorage`)
Multi-store filter — the single store dropdown is replaced by a checkbox panel listing all 20 stores; excluded stores are remembered in `localStorage`
Filtering is now client-side (no extra API round-trip on store change)
Best price/unit in brand grid and product pages is recalculated to reflect only visible stores
Removed
In-stock-only checkbox — the catalog API always filters out-of-stock products, so the checkbox was redundant
1.0.64 — 2026-04-12
Fixed
MaxDiaper: stock quantity text ("2 styk på lager") was incorrectly parsed as pack count. Fixed with a word-boundary anchor and negative lookahead rejecting stock context
1.0.62 — 2026-04-12
Fixed
Color variants (e.g. "MEGAMAX Pink" from MaxDiaper vs "MegaMax Pink" from CuddleKingdom) now group correctly — model keys are lowercased before indexing in the catalog tree
Fixed
Diaper-Minister: products using radio buttons instead of a `<select>` for size selection (e.g. NorthShore MegaMax) now have their sizes detected via a new radio-button fallback (tier 5)
1.0.60 — 2026-04-11
Fixed
Dalille: products with "Dobbeltpakke" in the title are now skipped during scraping and removed from the database
Dalille: "Kan ikke bestilles" (unavailable) variants are now skipped; existing DB records are marked out-of-stock automatically after each scrape
Admin: `/admin/hidden` internal server error fixed (missing `html.escape` import)
Stats: product count now reflects only in-stock products, matching the catalog view
1.0.58 — 2026-04-11
Added
Out-of-stock persistence: after each scrape, products not returned by the scraper are automatically marked as out-of-stock in the database. Catalog always filters `in_stock = 1`
1.0.56 — 2026-04-11
Added
Color localisation: color names in model titles (Black, White, Blue, Pink, etc.) are stored in English in the database and translated to the user's chosen language at render time
NorthShore: all products classified as AIO by default
Color variants normalised across languages: Blauw/Blue, Roze/Pink, Wit/White, TieDye/Tie Dye/Multicolors all group together
Fixed
Pull-Ons products now correctly classified as Pullup (regex fix)
1.0.53 — 2026-04-10
Fixed
NorthShore MegaMax from MaxDiaper now correctly grouped under brand NorthShore, model MegaMax (previously "MegaMax" was extracted as the brand)
1.0.51 — 2026-04-10
Added
MaxDiaper: brand extracted from the "Tillverkare" (Manufacturer) field on product detail pages — resolves ABU products (TinyTails, AlphaGatorZ etc.) being mis-attributed
Fixed
Brand canonicalisation expanded: AlphaGatorZ, Cloth-Backed, Cushies, TinyTails, Peekabu all mapped to ABU; LittleForBig/LFB merged
1.0.47 — 2026-04-10
Added
Tykables, LFB, Crinklz, Rearz, NorthShore, ABU all default to AIO category
1.0.46 — 2026-04-10
Added
CuddleKingdom: "Los" (single-piece) products are now skipped instead of being listed with wrong pack count
1.0.43 — 2026-04-10
Added
Unified `/admin` dashboard with links to all admin tools and per-store price-history deletion
Brand canonicalisation: ABUniverse → ABU, Better/BetterDry → BetterDry, LFB/LittleForBig → LFB
1.0.39 — 2026-04-09
Added
Color words normalised to English in `clean_model_name()` — Dutch, German, Swedish, Danish, Norwegian color names translated before noise stripping (e.g. "Zwart" → "Black")
1.0.36 — 2026-04-09
Added
20 stores scraped — added AB-Versand, Dalille, Apopro, Med24, MaxDiaper, CuddleKingdom, Cloudrys, Invizi, ABDL-TB-Club-Shop, Europe-Incontinence
Per-scraper documentation in `docs/scrapers/`
ModelAlias system: admin can create manual aliases to merge model names across stores
Flat list view with size filter and capacity (ml) column
`capacity_ml` field: absorbency in ml, shown in list view and product detail
1.0.14 — 2026-04-07
Added
Product category system (AIO, Pullup, Booster, Pad, Clothing, Gear, Other) with filter checkboxes and admin override per model
Price history graphs: 90-day EUR/unit trend per model using Chart.js
Scrape status dashboard with live polling and log viewer
Per-store scrape trigger from admin footer
1.0.5 — 2026-04-06
Added
5-language i18n (EN, DA, NO, SV, DE) with localStorage persistence
Auto-select currency from language (DKK for Danish, SEK for Swedish, etc.)
NOK currency
Disclaimer banner
Version badge + auto cache-busting via git pre-push hook
1.0.1 — 2026-04-05
Added
Initial release
10 stores: SaveExpress, Deutschland-Inkontinenz, LittleForBig, Diaper-Minister, ABDLFactory, NRU, ABUniverse, Abena Privat, TENA Danmark, Voksenbleer.dk
Brand → Model → Size drill-down navigation
Multi-currency support (EUR, DKK, SEK)
Admin auth, site password gate, model hiding
Docker + nginx-proxy deployment