Nyheter & Uppdateringar

v6.3.0
v6.3.0 Drift-detektion, veckovis full diff-sync & manuell inprisredigering 2026-04-21 Aktuell

32 commits sedan v6.2.0. Största lyftet: Shopify-drift som uppstår utanför PIM (manuell admin-edit, tredjepartsappar, webhooks) fångas nu strukturellt via veckovis full diff-sync + korrekt auto_pushed_at-stämpling. Grouping-validatorn slutar nagga på korrekt grupperade brand-alias. Inpris kan ändras direkt från produktdetaljsidan.

Shopify-drift — strukturell fix

  • Rotorsak hittad: auto_sync_changed_products filtrerade på Product.updated_at > last_sync → när Shopify redigerades utanför PIM (manuell titel-/prisändring, tredjepartsapp) blev driften osynlig tills PIM-raden råkade ändras av annan anledning
  • auto_sync_changed_products(force_all=True) skippar updated_at-filtret och 2000-raders-capet → diff-syncar varje PUSHED parent/standalone
  • Ny Celery-task full_diff_sync_shopify (Redis-lock, 6h time_limit) — schemalagd söndag 10:00
  • Admin-endpoint POST /api/shopify/admin/full-diff-sync för one-shot-trigger
  • _diff_update_product stämplar nu auto_pushed_at på både parent och varianter efter varje lyckad diff-sync (inkl. no-op) → incremental-filtret updated_at > auto_pushed_at fungerar nu som tänkt
  • Verifierad IRL: Swanson Shilajit 400mg 60 Vcaps — Shopify-titel "30 Caps" + pris 63 SEK medan PIM hade 182 SEK, fixades via /admin/update-price/{id} + efterföljande full diff-sync

Grouping-validator — slutar klaga på korrekt grupperade alias

  • Canonicalize brand-namn via brand_service.normalize_brand innan mismatch-flaggning → "Scitec" ↔ "Scitec Nutrition", "Now" ↔ "NOW Foods", "SIS (Science In Sport)" ↔ "Science In Sport", "Conteh" ↔ "Conteh Sports", "Carlson" ↔ "Carlson Labs" räknas inte längre som mismatch
  • Purge-pass rensar stale pending UNGROUP-suggestions för re-validerade varianter som inte längre är "wrong" (canonicalize matchar, eller verdict bytte till correct/uncertain). Rör aldrig user-accepted/ignored beslut, och rör bara in-scope-varianter så out-of-scope (push_status-filter) är säkra

Workshop-UI

  • Åtgärder-ikoner baseras på push_readiness istället för raw has_*-flaggor → stämmer med backendens push-status
  • push_missing_fields visar alla push-blockande fält, inte de 8 första
  • Produkter med WMS-lager men utan feed visar "WMS" som leverantör (tidigare tomt)

Shopify push — robusthet & bildhantering

  • Bildordning bevaras exakt på push — primary → gallery → scraped, med explicit position och sekventiell uppload (inte parallell) så Shopify inte omorganiserar
  • /shopify/resync-images/{id} endpoint för att fixa existerande fel ordning utan full re-push
  • Cache av trasiga bild-URL:er + robust 404-/deadlock-hantering
  • Short-circuit på 250-media-gränsen i Shopify (annars 422-loop)
  • DUBBLETTSKYDD: barcode cross-check → multi-variant-grupper nukas inte av misstag
  • DUBBLETTSKYDD: orphan-cleanup körs nu på alla push-vägar, inte bara create
  • /shopify/fix-duplicate-title/{id} för säker titel-rensning
  • /shopify/touch-product/{id} bust:ar Shopify page cache
  • /shopify/check-publications/{id} verifierar sales channel membership
  • /shopify/diagnose-availability/{id} för felsökning när produkter är osynliga

Pipeline & pricing

  • Pipeline: guard mot skalär faq_json i jsonb_array_length (krashade på ogiltig data)
  • pricing_engine.calculate_price fallbackar till globala regler när active_supplier_id IS NULL men wholesale_price_euro > 0 → produkter utan leverantör får ändå pris
  • name_completion har en enkel dedup-safety-net vid step exit
  • Fix: duplicate size suffix i name_completion

WMS & leverantörssystem

  • Ghost-import länkar nu produkter till virtuell WMS-supplier (id=6)
  • Slow-mover-rabattens UI-förbättringar och skyddsspärrar
  • /api/products/reconcile-orphan-suppliers quality-endpoint för att återbinda föräldralösa

Produktdetaljsida

  • Ny: manuell redigering av inköpspris (wholesale_price_euro) via penn-ikon bredvid "Inköp (EUR)"
  • Triggar pricing_engine.calculate_price(apply=True) automatiskt → säljpris + marginal räknas om
  • Respekterar existerande price_locked-logik (auto-unlock när wholesale drivit)

Push readiness

  • Parent name_quality-check skärpt — fångar dåligt namngivna föräldrar som annars slipper igenom
  • Fix: orphan-varianter visades inte i Standalone + Groups-sökning
v6.2.0 Image Guard, Namnrensning, EAN-integritet & Konkurrentrotation 2026-04-16

Stor städsession: 52+14 bildlösa produkter borttagna från Shopify, 758 namn rensade (dubbletter + imperial→metrisk), EAN-dubblett-rotorsak fixad med 3-lagers försvar, konkurrentrotation dynamiskt balanserad.

Image Guard — inga produkter utan bild till Shopify

  • push_group(): bildkontroll gäller nu även uppdateringar (inte bara nya)
  • _add_variant_to_shopify_product(): blockerar varianter utan bild
  • enrich_then_push(): standalones utan bild sätts till NOT_READY istället för auto-godkänd
  • 66 bildlösa produkter borttagna från Shopify + satta till NOT_READY
  • 7 openfoodfacts + 2 walmart + 3 shld.net placeholder-bilder rensade från PIM

Namnrensning — rotorsak fixad

  • Rotorsak: XML-feed appendade storlek/smak blint → "120 Mega Caps - 120 Mega Caps"
  • 3-lagers försvar:

1. Källa (xml_feed.py): Semantisk size-check via UNIT_SYNONYMS — "120 Mega Caps" = "120 Caps"

2. Skapande (import_engine.py): dedup_name() vid product creation

3. Pipeline (naming.py Pass 2c): Tar bort fristående "Sgels"/"90" absorberade av "90 Sgels"

  • Ny utility: UNIT_SYNONYMS + extract_size_tuples() i utils/text.py (50+ enhetssynonymer)
  • strip_imperial_units(): oz/lbs/fl oz → g/ml/kg i produktnamn
  • 758 produktnamn fixade i PIM, 186 Shopify-titlar uppdaterade
  • Svagare _dedup_name_parts() ersatt med dedup_name() i name_completion.py

EAN-integritet — inga dubbletter

  • Alla importers (import_engine, xml_feed, powerbody, WMS ghost) kollar nu alla produkter (aktiv + inaktiv) vid EAN-uppslag
  • Inaktiv produkt med samma EAN reaktiveras istället för att skapa dubblett
  • 465 inaktiva EAN-dubbletter raderade (data migrerad till aktiva versioner först)
  • Produktsökning default is_active=true — inaktiva döljs i UI

WMS

  • X-SKU blockering: Artiklar med articleNumber som börjar på "X" filtreras bort vid sync + ghost-import
  • 2 befintliga X-SKU-produkter inaktiverade

Konkurrentbevakning (PriceRunner)

  • Dynamisk slot-allokering: Fördelar daglig kapacitet baserat på kö-storlek istället för fasta procent
  • Ny Priority 4: Produkter med competitor_count=0 omkollas var 14:e dag
  • 10,089 aldrig-kollade produkter nås nu inom ~2 dagar (tidigare 4+)

Workshop Stats

  • Fix: Grupper med inaktiv parent-shell räknades som "att godkänna" i stats men visades inte i listan

Tester

  • 660 tester passerar (upp från 596)
  • 7 image guard, 13 namnrensning, 5 extract_size_tuples, 4 Pass 2c, 4 xml feed size check

---

v6.0.0 Ingredienser, Brands, WMS & Push-fixar 2026-04-15

Stor session: ingrediensberikning fixad för 200+ produkter, brand-konsolidering (2167 produkter), WMS-redesign med rabatt-funktion, push-fixar för standalones/non-food, bildkrav, polsk→svensk översättning.

Ingrediensberikning (Step 10)

  • Base64-bilddata i ingredients-fält skippas nu (PowerBody)
  • Strukturerad HTML i fel fält flyttas direkt utan AI-anrop
  • "Fullständig ingredienslista" rensas istället för att avvisa hela svaret
  • Fler placeholder-mönster: "Ej tillämpligt", "ingredients available as image"

Brand-konsolidering

  • 273 brand_mappings (155 nya + 118 uppdaterade)
  • 2167 produkter fick korrigerat brand-namn
  • Alla parenteser borttagna — 742 rena brands
  • Ny sync_vendors Celery-task: bulk-uppdaterar Shopify vendor-fält
  • Blocklist: Zielko, Zielako + 16 rengörings-keywords → 39 produkter avaktiverade

Dubblerade produktnamn

  • "60 Caps - 60 Caps" och "100 Sgels - 100 Softgels" fixas vid import + pipeline steg 1
  • 182 befintliga dubbletter rensade

WMS — Redesign

  • Berika & Pusha: En knapp istället för två — enrich_then_push gör allt
  • Långsamrörligt rabatt: Inline prisredigering, snabbknappar -10/-20/-30%, bulk-rabatt
  • POST /wms/discount + /wms/remove-discount
  • Sorterbara kolumner (lagervärde, kostnad, pris, marginal)
  • Checkboxar med "rabattera alla valda"
  • Ghost-import: "Sök & importera via EAN" — skapar minimala PIM-produkter från WMS-data (👻)
  • Redis dedup-cleanup före push

Push-fixar

  • Standalones (non-food) inkluderas i push_approved_unpushed
  • Non-food: hoppar över ingredients-check i push_product
  • Bildkrav: standalone/variant utan image blockeras från push
  • Grupp-push: blockeras om ingen variant har bild
  • Shopify-felrapportering: faktiskt HTTP-fel istället för "Tomt API-svar"

Översättning

  • 1144 polska texter → svenska (288 usage + 808 warning + 48 ingredients)

API

  • seo_title, seo_description, short_description inkluderade i list-endpoint

Automatisk lagerrensning (v6.1.0, 2026-04-16)

  • Auto-rabatt baserat på lagerdagar från WMS API (/api/v1/articleItemsinDate)
  • 30+ dagar → -10%, 60+ dagar → -20%, 90+ dagar → -30%
  • Konfigurerbart via pim_settings.discount_tiers (JSON)
  • Marginalskydd: aldrig under kostnadspris (minst 5% marginal)
  • Körs dagligen 07:00
  • Shopify "Lagerrensning"-collection — skapas automatiskt, produkter läggs till vid rabatt
  • Auto-restore vid slutsålt — spårar discount_units_remaining (sålda enheter)
  • Nya inleveranser ignoreras — bara de gamla enheterna räknas
  • Exempel: 47 st rabatterade. 10 nya kommer in. 47 säljs → pris återställs. 10 nya säljs till ordinarie.
  • Produkt tas bort från Lagerrensning vid restore
  • Manuell rabatt — inline prisredigering + snabbknappar -10/-20/-30% + bulk
  • Sorterbara kolumner i långsamrörligt-tabellen

Tester

  • 596 tester passerar (upp från 320)
  • CI-fix: nexus.db.session mock för miljö utan DATABASE_URL

---

v5.0.0 Vision Corrections, Auto-Gruppering & Shopify-reparation 2026-04-08

Storskalig datakvalitetsforhojning: Vision OCR-korrektioner applicerade, brand-dedup, auto-gruppering av standalone-produkter, Shopify-synk reparerad, Workshop-push forbattrad.

Vision Audit & Corrections

  • Applicerade 12,371 namnkorrektioner fran Vision OCR (ean_truth)
  • 2,699 felplacerade varianter fixade (114 flyttade, 2,585 avgrupperade)
  • Alla 26,711 produkter vision-verifierade, 0 pending

Brand Consolidation

  • 12 brand-dubbletter fixade (BioTechUSA, MyVita, Better You, Scitec m.fl.)
  • 908 produkter uppdaterade med kanoniskt brand-namn
  • 115 rena brands, 0 dubbletter kvar
  • NOW Foods (NOW Sports) och Universal Nutrition (Animal) fixade

Auto-Gruppering

  • Ny group-standalone endpoint: grupperar standalone-produkter per brand + base-name
  • 1,193 nya grupper skapade, 3,279 produkter grupperade
  • Utokad smak-regex: 40+ smaker, word boundaries, storleksmonster
  • Variant-namn fran ean_truth (suggested_variant_name, flavor/size/strength)
  • fix-variant-names-in-shell-groups: batch-reparation av 405 felaktiga variant-namn

Enrichment & Quality

  • 7,569 produkter berikade (beskrivning, SEO, ingredienser, collection)
  • Quality sweep kort: 535 grupper aterkapade, 613 namn normaliserade
  • 11,426 polska produktnamn oversatta
  • 1,107 tomma needs-attention produkter raderade (bulk-delete med FK-cleanup)

Shopify-synk & Reparation

  • shopify-repair endpoint: aterstallde 287 push-status, rensade 61 grace periods
  • 31 produkter med lager aterstallda fran draft pa Shopify
  • shopify-health diagnostik-endpoint (grace period, drafted, stock-zeroed)
  • shopify-affected-24h endpoint med konfigurerbar tidsperiod
  • match-eans endpoint for bulk EAN-uppslag mot PIM

Workshop-forbattringar

  • Push-feedback: tydlig toast nar push blockeras (saknade falt, exkluderade varianter)
  • Ingen tyst miss langre — _try_auto_push_group returnerar detaljerad dict
  • set-shell-parents-ready: satter nya grupper till READY for granskning
  • unapprove-shell-groups: rensar approval sa nya grupper syns i Workshop

Pipeline & Infrastruktur

  • force parameter fixad i pipeline API (var tyst bortkastad)
  • Grace period-check AVSTANGD (orsakade Google Merchant-forsvinnanden)
  • undo-group-standalone endpoint for att anga gruppering
  • bulk-delete-products endpoint med FK-cleanup

Bugfixes

  • Worker fixes: event loop, Shopify 409, PUSH_FAILED infinite retry
  • Brand parentes rensade (34 brands)
  • Pipeline step dependency validation fungerar nu med force=true
v4.0.0 Produktion, stabilitet & kvalitetsautomatisering 2026-04-06

Storskalig stabilisering: 20+ commits, 9 kritiska buggar fixade, Shopify-katalogen utokad fran 8 086 till 9 029 produkter, automatisk kvalitetssäkring inford.

Kritiska buggar fixade

  • Stock-import double mapping_bulk_import() i xml_feed.py mappade falt dubbelt, vilket nollstallde lagersaldo for ~6 400 produkter fran BioU/Naturamed/Vitamin360. Orsak till forsaljningstapp och -3 000 produkter i Google Merchant
  • Pipeline steg 16 import re — Lokal import inuti funktion skuggade modul-import, kraschade alla produkter
  • PowerBody import session poisoning — En UniqueViolation forgiftade sessionen for alla foljande produkter
  • Celery maintenance_tasks ej registreradcleanup_orphan_products schemalagd men aldrig importerad
  • Quality audit MissingGreenlet — Saknade selectinload(Product.parent_product) vid async access
  • Ingredient backfill session crash — Saknade rollback per produkt + per batch
  • Ingredient form VARCHAR(100) overflow — Truncering saknade
  • Suggestion engine duplicate hash — IntegrityError vid flush
  • Grouping validator HTTP URLs — HTTPS-filter saknades, skickade HTTP till Anthropic API

Feed Presence — Sakerhetssystem

  • min_feed_count_ratio hojd fran 0.5 till 0.85 — blockerar deaktivering om import tappar >15%
  • Max-tak per korning: 100 offers deaktiverade, 50 stock-nollstallningar, 50 draftningar
  • Anomali-larm: PIM-notis vid partiell feed, mass-forsvinnande, cap-traffar
  • feed_grace_days satt till 60 (var NULL/odefinierat)

Shopify Sync — Nya verktyg

  • full_sync_shopify — Bulk re-push med push_group/push_product (exakt som Workshop), Redis resume + auto-restart via Beat
  • restore_shopify_stock — Aterstaller Shopify-lager fran PIM stock, Redis resume
  • auto_sync_shopify uppgraderad: 400/korning var 10 min (var 200/30 min), Redis dedup-lock
  • Resume-checker (var 5 min) — Beat kontrollerar Redis for avbrutna tasks, startar om automatiskt
  • Alla long-running tasks har nu resume-stod (Redis SET eller BatchJob DB)

Grupperingskvalitet

  • Ingrediens-differentiator — ~50 nyckelord (mineralformer, probiotikastammar, vitaminformer) blockerar felgruppering av samma-brand-produkter
  • Skyddar i 4 lager: kandidatsok → AI-prompt → sanity check → validator
  • _name_word_overlap fixad: max() istf min() i namnare — korta parent-namn far inte langre artificiellt hog overlap
  • DECIDE_GROUPING prompt utokad med explicita regler for ingrediensformer

Kvalitetssystem

  • Shopify Quality Sweep (ny task, sondagar 08:00) — kontrollerar ALLA pushade produkter:
  • Fas 1: PIM-datakvalitet (polska namn, non-AI desc, saknade ingredienser/SEO/FAQ)
  • Fas 2: Shopify-verifiering (hamtar live-data, jamfor titel/pris/status/desc)
  • Auto-fixar via pipeline + schemalagger auto_sync for push
  • Pipeline steg 21 undantagen fran only_new — oversatter polska namn varje natt
  • Parent readiness fix — Parents utan EAN behandlas korrekt som parent shells (blockas inte langre som standalone)

Produktkatalog — Expansion

  • +943 Shopify-produkter (8 086 → 9 029 aktiva)
  • +3 428 produkter med lager (792 → 4 220)
  • 1 644 bilder kopierade fran variant till parent
  • 273 grupper validerade och godkanda
  • 1 035 non-AI beskrivningar ersatta med AI-genererade

Pipeline & Prestanda

  • DB connection pool dimensionerad for Render: 10+30=40 per process, max 80 totalt (av ~97)
  • Pipeline concurrency cappat vid 30 (matchar poolstorlek)
  • Per-coroutine sessions bekraftade som redan implementerade

UI

  • Schemasida dynamisk — genereras fran TASK_META + beat_schedule, uppdateras automatiskt nar tasks laggs till
  • Quality sweep endpoint: POST /api/quality/quality-sweep
  • Full sync endpoint: POST /api/workshop/full-sync-shopify
  • Restore stock endpoint: POST /api/workshop/restore-shopify-stock
v3.0.0 Säkerhet, prestanda & kvalitetshärdning 2026-03-25

Fullständig kodgranskning av hela systemet — 100 förbättringar i 16 commits.

Säkerhet (14 fixar)

  • SQL Injection: Parameteriserade queries i konkurrentanalys, WMS stock-import och Shopify GraphQL (3 kritiska)
  • Autentisering: Lagt till auth på 6 oskyddade endpoints (notifikationer, blocklist, merchants, dashboard-stats)
  • XSS-skydd: Escapade supplier_url i workshop-templates
  • IDOR: Ägarskapscheck på AI-konversationer — användare kan inte längre läsa andras chattar
  • Timing-safe: API-nyckel-jämförelse med hmac.compare_digest() istället för ==
  • Token-expiry: optional_auth validerar nu token-utgång
  • Sort-whitelist: Produktlistor tillåter bara fördefinierade sortkolumner (förhindrar info-läckage)
  • HTTP-metoder: batch_reprice ändrad från GET till POST (muterade data via GET)
  • Temp-endpoints borttagna: /queue-status-open och /run-english-cleanup (debug-endpoints utan auth)

Prestanda — Databas (10 fixar)

  • 9 nya index: shopify_product_id, shopify_variant_id, is_active, updated_at, ai_processing_status, price_history.product_id, ai_messages.conversation_id, notifications(is_read, created_at), pricing_rules(supplier_id, priority)
  • Settings-cache: get_setting() cachas i 60s med automatisk refresh — eliminerar tusentals DB-queries per minut i hot paths (prissättning, pipeline, push)
  • Regex pre-kompilering: 100+ mönster i quality_audit.py kompileras en gång vid modulstart istället för per anrop
  • Batch-prissättning: batch_price_all() processar i chunks om 500 istället för att ladda alla produkter i minnet

Prestanda — API & Shopify (10 fixar)

  • Persistent HTTP-klient: Claude API, Shopify API, WMS API och AI-chatten återanvänder TCP-anslutningar (connection pooling) istället för ny handshake per request
  • Shopify 5xx retry: Automatisk retry med exponentiell backoff vid serverfel (500/502/503)
  • N+1 fixar: toggle_price_lock, toggle_competitor_exclusion, dismiss_priority — batch-load med WHERE id IN (...) istället för per-ID queries
  • Redis connection pool: Shopify EAN-cache använder delad Redis-anslutning istället för ny per anrop

AI Pipeline (5 fixar)

  • Token-tracking: Kumulativ spårning av input/output-tokens per process. Nytt API: GET /api/pipeline/token-usage
  • Blocklist-cache: Import-motorns keyword-check laddar listan en gång per import istället för per produkt
  • Valutasynk: currency.py läser nu EUR/SEK- och VAT-rate från DB-settings (var hårdkodade till 11.5/0.12)
  • Central RRP: _add_variant_to_shopify_product använder nu generate_compare_at_price() — samma logik som batch-push

Buggfixar (12 fixar)

  • JSONB dirty tracking: mark_as_edited() reassignar dict istället för in-place mutation — fixar att SQLAlchemy inte alltid upptäckte manuella redigeringar
  • BatchJob.updated_at: onupdate=func.now() istället för datetime.now (naiva timestamps i timezone-kolumn)
  • Hårdkodade rates (4 ställen): VAT 1.12 och EUR/SEK 11.5 i WMS, konkurrentanalys och competitor_price_service — nu läses från DB-settings
  • Export-pagination: Total-räknare var fel vid post-filter på produkttyp (grupp/standalone)
  • Toast-färger: Error-toasts visades i grönt — nu röd=fel, blå=info, gul=varning, grön=ok
  • deleteProduct: Visade "Produkt raderad" oavsett om API:t returnerade fel
  • Sync Redis: _acquire_group_lock körde synkront Redis i async-kontext — nu via asyncio.to_thread()
  • Duplicate push: _assign_product_category anropades dubbelt i push_product()
  • Starlette 1.0.0: Alla 19 TemplateResponse-anrop uppdaterade till ny signatur (request, name, context)

Infrastruktur (12 fixar)

  • .dockerignore: Förhindrar att .env, .git/ och __pycache__/ kopieras till Docker-imagen
  • Dockerfile non-root: Container kör som appuser istället för root
  • Render SECRET_KEY: Web och worker delar nu samma nyckel (var separata generateValue)
  • Celery retry: push_products, push_group_task och enrich_then_push har nu autoretry_for, retry_backoff och max_retries=3
  • Celery soft_time_limit: Alla tasks utan soft limit har nu fått 20s varning innan hard kill
  • Pinnade dependencies: Alla paket har nu övre versionsgräns () — förhindrar breaking changes
  • HTMX borttagen: Laddades på varje sida men användes aldrig (dead weight)
  • Version synkad: config.py och pyproject.toml använder samma version
  • .env.example komplett: Lagt till ANTHROPIC_API_KEY, Shopify OAuth, API-nycklar, WMS-credentials

Kodkvalitet (14 fixar)

  • variant_service.py decomposition: 1987 → 1453 rader. Nya moduler:
  • variant_move.py (319 rader) — move_variants() med brand-validering och Shopify-cleanup
  • variant_push.py (264 rader) — auto-push-logik, innehållsrensning, readiness-checks
  • Alla funktioner re-exporteras från variant_service.py (inga importer behöver ändras)
  • _diff_update_product decomposition: 375 → ~200 rader. Extraherade:
  • _diff_product_fields() — produktfält-jämförelse (titel, desc, vendor, tags)
  • _build_all_metafield_inputs() — metafield-assembly för bulk GraphQL
  • Shared helpers: build_shopify_title() (3 kopior → 1), name_patterns.py (duplicerade regex → 1 modul), get_task_redis() (2 kopior → 1)
  • JS-dedup: esc(), escAttr(), timeAgo() definierades 6-7 gånger i olika templates — nu en gång i app.js

Frontend & UX (13 fixar)

  • Dark mode: Toggle-knapp i topbaren (måne/sol-ikoner). Sparas i localStorage, respekterar prefers-color-scheme. Dark styles på body, header, main content.
  • Breadcrumbs: Produktsidan visar Produkter / Brand / Produktnamn
  • AbortController: Sök i produktlista och workshop avbryter föregående API-anrop vid snabba ändringar
  • URL state management: Filter på produktlistan (sök, brand, leverantör, sortering, sida) sparas i URL:en — refresha behåller filtren, dela länken funkar
  • beforeunload-varning: Varnar om du navigerar bort medan auto-save väntar (1.5s debounce)
  • Tillgänglighet: Skip-to-content-länk, ARIA-label på hamburger-meny, id="main-content"
  • Login accent: Konsekvent accent-färg (#0f3460) mellan login och övriga sidor
  • Shopify store URL: Dynamisk template-variabel istället för hårdkodad 4bdabb-d9
  • Error-loggning: Tomma catch(e) {} i base.html loggar nu till console.debug

Testsvit (ny)

  • 40 tester i 5 moduler — alla kör utan databas eller externa tjänster (~0.7s):
  • test_utils.py — EAN-validering, normalisering, generering, valutakonvertering, name patterns (16 tester)
  • test_quality_audit.py — HTML-strippning, bad patterns, non-Swedish detection (6 tester)
  • test_mapper.pybuild_shopify_title edge cases (5 tester)
  • test_manual_edit_protection.py — JSONB tracking, mark/can_update/safe_update (8 tester)
  • conftest.py — Delade fixtures

Tailwind pre-build (förberedelse)

  • tailwind.config.js, static/css/input.css och package.json skapade
  • Kör npm install && npm run css:build för att generera kompilerad CSS (ersätter CDN)

---

v2.0.0 Grupp-push 2.0, Dubblettskydd & WMS-förbättringar 2026-03-23

Grupp-push — Omskriven och robust

  • Recreate-from-scratch: När majoriteten av varianter saknas på Shopify raderas den gamla produkten och en ny skapas med ALLA varianter i ett enda batch-POST
  • Variant-ID-synk: Automatisk mappning av shopify_variant_id via EAN-matchning när varianter finns på Shopify men PIM saknar ID:n
  • is_active=NULL-fix: Varianter med is_active=NULL (istället för True) filtrerades bort i push_group — nu inkluderas de korrekt (isnot(False))
  • Stale ID-rensning: Auto-clear av Shopify-ID:n som returnerar 404
  • WMS batch per grupp: Push batchar nu per GRUPP (parent + barn) istället för godtyckliga chunks om 20
  • API: POST /shopify/admin/repush-group/{id} — tvinga push oavsett status
  • API: POST /pipeline/regenerate/{id} — rensa innehåll + starta berikning

Shopify dubblettskydd

  • Titel-baserad dedup på ALLA push-vägar (standalone + grupp) — _find_shopify_product_by_title() kollar om en Shopify-produkt med samma titel redan finns innan ny skapas
  • EAN-konflikt-kontroll vid approve — blockerar om EAN redan finns på annan Shopify-produkt
  • Per-parent Redis-lås förhindrar parallella pushes som skapar dubbletter
  • Variant-namn-dedup: [EAN]-suffix vid duplicerade option-namn (t.ex. "Marjoram - 9ml [EAN]")
  • Admin-verktyg: POST /shopify/admin/delete-product/{id}, duplikatskanner, metafield-diagnostik

Produktverkstad — Variant-namngivning

  • Fix: 3:e standalone → grupp fick inte variant_name korrekt beräknad
  • Strip-logik: om parent-namn inte matchar som prefix, strippas brand-prefix ("Brand - ") istället
  • Auto-reprefix: Om variant_name fortfarande är hela produktnamnet efter flytt, räknas gemensamt prefix om för alla barn, parent-namn uppdateras, och alla variant_names beräknas om
  • Bulk-fix: POST /variants/fix-variant-names — skannade och fixade 101 varianter i 86 grupper
  • Fix: stat-kort-markering — "Redo att pusha" markerade "Pushade" pga felaktigt index i typeMap

WMS-prioritetslista

  • Inline-redigering: vikt, pris, namn, is_food direkt i listan
  • Varianter vars parent redan finns på Shopify exkluderas från listan
  • Korrekt push-count (push-ready produkter, inte totalt med varianter)
  • Föräldra-arv: varianter ärver beskrivning/ingredienser-status från parent
  • Tyst uppdatering — listan blinkar inte längre vid push

CBD/Hemp Oil — Blockerade

  • 36 CBD/Hemp Oil-produkter raderade från PIM och Shopify (5 Shopify-produkter)
  • Blocklist: cbd och hemp oil som keywords — förhindrar framtida import

Buggfixar

  • Deadlock i auto_sync + batch_push_all_prices (uppdaterade samma rader)
  • Metafield-typer matchar exakta Shopify-definitioner (FAQ=json, videos=single_line_text_field)
  • Collection "already exists" — nedgraderad från ERROR till DEBUG
  • Ghost-pushed varianter (PUSHED utan Shopify-ID) — kontrollerar shopify_variant_id innan markering
  • ShopifyClient skapas före title-check i _push_group_inner
  • Tomma varianter (utan EAN) auto-deaktiveras vid merge

---

v1.9.0 Batch-arkitektur, Språkrensning & Auto-sync 2026-03-21

Resilient Batch-arkitektur

  • BatchJob DB-modell med heartbeat-baserad liveness-detektion
  • Alla långkörande tasks (push_all_prices, push_all_content) processar 500 items/batch
  • Self-continuation: varje batch köar nästa automatiskt
  • Resume checker var 5:e minut — auto-startar avbrutna jobb
  • Worker-crash → automatisk återupptagning från senaste checkpoint
  • Dedup-lås på push_all_prices (bara en åt gången)
  • Separata Celery-köer: imports, bulk, celery — blockerar aldrig varandra
  • Workers ökade från 4 till 6
  • API: GET /batch-jobs, POST /batch-jobs/{id}/cancel

Språkrensning — Polsk text borttagen

  • 1378 produktnamn översatta från polska/tyska till engelska via steg 21
  • AI-driven detektion — Claude bedömer om namn är engelska (fångar ALLT)
  • 658 beskrivningar, 59 korta beskrivningar, 18 SEO-titlar med polska rensade + regenererade
  • 1897 polska usage-instructions rensade + regenererade på svenska
  • System-prompt förstärkt: "ALL text ska vara KORREKT SVENSKA. ALDRIG polska."
  • _has_foreign_language(): 40+ nya polska ord + polsk-teckendetektion
  • Förhindrar framtida polska i AI-genererat innehåll

Livsmedels-kategorisering

  • 13 nya mat-kategorier i CATEGORIZE-prompt (Bakning, Fett & olja, Kryddor, etc.)
  • Shopify taxonomy-mappning: fb-* (Food & Beverages) istället för hb-* (Health)
  • 813 matprodukter omkategoriserade (t.ex. citronjuice → "Övriga livsmedel")
  • Livsmedelsregler i prompt: juice/olja/smör → Livsmedel, INTE Kosttillskott

Standalone metafields till Shopify

  • Ingredienser, usage, nutrition, warning pushas nu på BÅDE produkt- OCH variant-nivå
  • Temat läser produkt-metafields för standalone, variant-metafields för grupper
  • Diagnostik-endpoint: GET /api/shopify/check-metafields/{product_id}

Auto-sync alla fält till Shopify

  • Ingen 200-produktlimit längre — alla ändrade produkter synkas
  • updated_at > auto_pushed_at detekterar osyncade ändringar
  • _diff_update_product jämför ALLA fält: pris, namn, beskrivning, SEO, ingredienser, tags
  • Var 30 min automatiskt via Celery Beat

Worker-hantering

  • Render API-integration: deploy + restart worker via API
  • POST /api/pipeline/purge-queue — rensa Celery-kön
  • POST /api/pipeline/revoke/{task_id} — döda specifik task
  • GET /api/pipeline/queue-status — visa aktiva/köade tasks
  • GET /api/shopify/search-category — sök Shopify taxonomy
  • Auto-deploy AV på worker (manuell deploy via Render API)

Pipeline-förbättringar

  • Steg 15 concurrency: 100→10 (fixar asyncpg session-konflikter)
  • Steg 21: AI-detektion av icke-engelska namn (inte bara ordlista)
  • normalize_units: punkt efter produktformer borttagen (Caps. → Caps)

---

v1.8.0 Prisfix, Prestandaoptimering & Konkurrentbevakning 2026-03-20

KRITISK: Inköpspris-bugg fixad (BioU + Naturamed)

  • priceAfterDiscountNet i SolEx-feeden tolkades som PLN istället för EUR
  • Alla ~9000 BioU/Naturamed-produkter hade 4.3× för låga inköpspriser
  • Fix: bara retailPriceGross (RRP) konverteras från PLN, wholesale lämnas som EUR
  • Alla priser omräknade och pushade till Shopify

Prestandaoptimering

  • Dashboard: 20+ separata COUNT-queries → 1 SQL-query med FILTER
  • Standalone stats: 7 queries → 1 raw SQL-query
  • AI-förslag: N+1-query för target-varianter → batch-query
  • Bulk accept: nytt /suggestions/act-bulk endpoint — en request, en cleanup

Standalone approve → auto-push

  • Godkända standalone-produkter triggar automatiskt enrich_then_push
  • Berikar saknade fält (beskrivning, ingredienser, SEO, pris) → pushar till Shopify
  • Progressbar visas under bearbetning
  • Blockerar bara på saknat EAN — allt annat genereras automatiskt

Namnstandardisering

  • Kaps.Caps, vcapsVcaps, Tabl.Tabs etc.
  • Punkt efter produktformer borttagen: 120 Caps.120 Caps
  • Steg 1-4, 16-17 (regex/normalisering) undantas från only_new-filter
  • Nya regler appliceras nu även på redan-processade produkter

Spök-pushade varianter

  • 66 varianter markerade som PUSHED utan Shopify-ID — resetade till NOT_READY
  • Fix: _diff_update_product markerar bara varianter med shopify_variant_id som PUSHED
  • Variant-namndedup: [EAN]-suffix vid dubblettnamn vid add-variant

Konkurrentbevakning (Pricerunner)

  • Första Pricerunner-körningen triggad (4152 priser insamlade)
  • Ny: Exkludera produkter från prisbevakning (multi-packs som felmatchas)
  • Toggle per produkt eller bulk i UI
  • Filter "Exkluderade" på konkurrentsidan
  • POST /api/competitors/toggle-exclusion
  • Exkluderade skippas i daglig rotation + statistik
  • Dashboard-stats optimerad: 6 queries → 1 SQL-query

Notifikationer

  • Spammiga success-notiser per push borttagna
  • Sammanfattningsnotis vid bulk-push (3+ produkter)
  • Progress-tracking i enrich_then_push och push_all_prices

API-förbättringar

  • Pipeline, imports, workshop bulk, smart-push, push-all-prices — alla accepterar admin_api_key
  • Pipeline: nytt supplier_id-filter för att köra steg på specifik leverantör
  • DB-migration: competitor_excluded boolean på products

Buggfixar

  • run_pipeline() saknande only_new-parameter → NameError i enrich_then_push
  • Step 11 pre-filter exkluderade produkter med collection men utan SEO-titel
  • enrich_then_push Phase 0 rensade inte structured_ingredients för standalone
  • push_group blockerade på saknade ingredienser → nu bara warning

---

v1.7.0 Nexus AI Chat 2026-03-19

AI-assistent

  • Claude-baserad AI-chat med affärsintelligensverktyg
  • 6 verktyg: produktsökning, statistik, prisanalys, konkurrentdata, lagerdata, pipeline-status

---

v1.6.0 WMS-integration (Ongoing Warehouse) 2026-03-18

Ongoing WMS — Lagerhantering

  • REST API-klient (nexus/wms/client.py) — artiklar, lager, ordrar, rörelser
  • Lagersaldo-synk var 30 min: 5358 WMS-artiklar → 4955 matchade mot PIM via EAN
  • Nytt fält: wms_stock_quantity på Product — fysiskt lager från WMS
  • Auth: HTTP Basic, Goods Owner ID 81 (Nutri), Lager: Staffanstorp

Lagerskydd — WMS blockar grace period

  • Om leverantör försvinner MEN produkten har WMS-lager → grace period startas inte
  • Produkten hålls aktiv på Shopify (vi har ju varan i hyllan!)
  • Kollas automatiskt i _handle_offer_loss()

Ghost-produkter

  • Hittar produkter i WMS som saknas i PIM eller Shopify
  • 229 hittade — varav 211 "Oly" och "HAIRtamin" (hanteras separat, dolda)
  • 15 inaktiverade PIM-produkter med WMS-lager (kan återaktiveras)

WMS Dashboard (/wms)

  • Stats-kort: artiklar i lager, enheter, ghost-produkter, långsamrörligt
  • Tab 1 Lagersaldo: sökbar produkttabell med WMS-lager
  • Tab 2 Ghost-produkter: WMS-artiklar som inte finns i PIM/Shopify
  • Tab 3 Långsamrörligt: högt lager + låg försäljning — rabattförslag
  • Sidmeny: "Lager" länk under Verktyg

WMS API-endpoints

  • GET /api/wms/status — anslutningsstatus + antal
  • GET /api/wms/stock — paginerad lagerlista
  • GET /api/wms/ghost-products — saknade produkter
  • GET /api/wms/slow-moving — långsamrörligt lager
  • POST /api/wms/sync — manuell synk
  • POST /api/wms/test-connection — testa anslutning

Inställningar

  • wms_enabled (toggle), wms_base_url, wms_username, wms_password, wms_goods_owner_id

---

v1.5.0 Dynamisk Prissättning, Notifikationer & Larm 2026-03-18

Competitor-Follow Pricing — Dynamisk prissättning

  • Nytt prisläge: price_mode = "competitor" — produkter följer konkurrenter automatiskt
  • Daglig auto-sync (kl 10:15): uppdaterar alla competitor-mode produkter
  • Om konkurrent sänker → vi följer ner (håller undercut-avstånd)
  • Om konkurrent höjer → vi följer upp
  • Marginal-golv: om undercut ger för låg marginal → sätts pris till min-marginal istället
  • Smart unlock: om grossistpris ändras → pris låses upp automatiskt för omberäkning
  • Nya fält: price_mode, competitor_undercut_amount, competitor_min_margin
  • 918 produkter applicerade med 5 kr undercut, 30% min marginal
  • Resultat: 679 sänkta, 239 höjda, 307 satta till marginal-golv

Prislåsning

  • Konkurrentmatchade priser låses automatiskt (skyddas från daglig priskörning)
  • Bulk lås/lås upp i UI via checkboxar + knappar
  • Lås-ikon (🔒) visas bredvid låsta priser i tabellen
  • API: POST /api/competitors/toggle-price-lock

Fler prisstrategier

  • Underbjud 1 / 5 / 8 / 10 kr
  • Underbjud 2% / 5%
  • Matcha lägsta
  • Ny parseStrategy() funktion parsar dropdown till API-params
  • Fungerar i både quick-match (per produkt) och bulk (Analys-tab)

Prisjämförelse — Snabbvy-flikar

  • 5 flikar: Alla / Vi är dyrare / Vi är billigast / Analys / Inställningar
  • Klickbara stats-kort: klicka "Vi är dyrare" → hoppar till fliken
  • Position/källa-filter dolda (ersatta av flikar)

Pricerunner — Jämför produktpris vs totalpris

  • Ny setting: competitor_compare_total_price (toggle)
  • Av (default) = jämför bara produktpriset
  • På = jämför totalpris inkl frakt

Notifikationssystem

  • Klocka med badge i header — visar antal olästa notiser
  • Dropdown med senaste notiser, färgkodade (röd/gul/blå)
  • 60-sekunders polling för nya notiser
  • Setting: notifications_enabled (toggle)
  • DB-modell: notifications tabell
  • API: GET /api/notifications, GET /count, POST /read

Automatiska larm

  • ❌ Import misslyckad → error-notis
  • ⚠️ Produkt draftad (grace period utgången) → warning-notis
  • ℹ️ Produkt återaktiverad → info-notis
  • ℹ️ Prisjustering applicerad → info-notis

Grace Period — Bugfix & Konfiguration

  • Fix: en variant utan leverantör draftade hela Shopify-produkten — nu kollas syskonvarianter först
  • Höjd till 30 dagar (alla leverantörer)
  • 1260 produkter återaktiverade + 170 drafts aktiverade på Shopify
  • Kopplad till default_grace_period_days setting (inte hardkodad)

Melatonin — Blockering

  • 49 melatonin-produkter hittade (namn + ingredienser)
  • 58 produkter inaktiverade i PIM
  • 23+ raderade från Shopify

Datamängd — Pipeline

  • FAQ: +696 parents genererade
  • Ingredienser: +175 structured_ingredients (med översättning)
  • Beskrivningar: +44 AI-beskrivningar
  • BioU-import verifierad och manuellt triggas

---

v1.4.0 Konkurrentintelligens, Prishistorik & Systemstädning 2026-03-18

Prisjämförelse — Actionable UX

  • Färgkodade rader: grön (billigast), gul (konkurrenskraftig), röd (dyrare)
  • "Matcha"-knapp per produkt (hover) — one-click prisjustering med valbar strategi
  • Marginal vid lägsta — ny kolumn visar vad marginalen blir om vi matchar
  • Fräschhetsprickar med datum-tooltip
  • 4 summary-kort i Analys: potential besparing, matchbara, avvikelse, billigast
  • Brand-analys med progress bars + sorterbara kolumner
  • Prisstrategi-kort: min marginal slider (5-50%) + strategi-dropdown (localStorage)
  • Daglig gräns höjd till 10 000 produkter (100 API-anrop, full kvot)

Pricerunner — Mer data

  • Nya fält: merchant_origin, offer_name, item_condition per offer
  • Produktmetadata: pricerunner_rank, pricerunner_url, pricerunner_category
  • Click & Cost API: get_click_cost_summary(), get_click_cost_by_product()
  • Produktfält: pricerunner_clicks_30d, pricerunner_cost_30d
  • Veckovis Celery-task (måndagar 11:00) för klickdata-sync
  • Fixar: delivery_time dict→string, dedup EAN+merchant, deadlock retry

Prishistorik-diagram (produktdetaljsidan)

  • Chart.js linjediagram med färgade prickar per händelsetyp
  • Grön = manuell, blå = regel, orange = konkurrentmatch, röd = leverantörsbyte
  • Tooltip: datum, gammalt→nytt pris, anledning, vem som ändrade
  • Tidsval: 30d / 90d / 365d
  • Händelselista under diagrammet med typ-badges

is_food — Icke-livsmedel skippar ingredienskrav

  • Nytt fält is_food (bool, default true), 268 produkter flaggade
  • Push readiness: skippar structured_ingredients för is_food=false
  • Pipeline steg 10 + 20: skippar icke-livsmedel
  • Auto-detect: scripts/detect_non_food.py (accessories, shakers, utrustning)
  • Skincare behåller is_food=true (behöver INCI-lista)

Varningstext + Användningsinstruktioner

  • Nytt fält warning_text — genereras i pipeline steg 10 tillsammans med ingredienser
  • Använder iHerb-skrapad suggested_use + warnings som AI-källa
  • Shopify metafields: c_f.usage-instructions (fixat bindestreck), c_f.warning (nytt)
  • Starkare svensk prompt + avvisar engelska svar i nutritional_info

Produktvideor

  • Nytt fält product_videos (JSONB lista YouTube-URLs)
  • 5623 produkter med videos (iHerb + Shopify metafield-pull)
  • Visas i bildsektionen på produktdetaljsidan med YouTube-ikon
  • Pushas som c_f.product_videos metafield

Shopify bildpush

  • scripts/push_images_shopify.py — parallella workers, skip/resume
  • 2346 produkter pushade med rätt bildordning
  • Bild 1 per variant = variantens huvudbild (kopplad via image_id)
  • Bilder 2-7 som delat galleri, inga duplicates, banners filtrerade

iHerb-skraper — Förbättringar

  • Bilder: button.thumbnail-item img[data-large-img] för korrekt ordning
  • Videos: YouTube-embeds från produktgalleri
  • Rankings: .best-selling-rank selector + fallbacks
  • 3880 EANs omskrapade, snabbare delay (50-150ms)
  • Foreign language-filter borttaget — AI översätter istället

Workshop-fixar

  • Default filter: "Att godkänna"
  • Workspace: bara ogodkända brand-grupper (backend + frontend filter)
  • "Acceptera alla grupp": paginerad fetch (per_page=200)
  • Standalone checkbox-fix (.sa-card.sa-row)
  • Toast: en grön meddelandefält, top-right, ett åt gången

Push-säkerhet — Ingredienskrav

  • push_product: blockerar standalone livsmedel utan structured_ingredients
  • push_group: blockerar grupp om ingen variant har ingredienser
  • enrich_then_push: readiness-check efter enrichment
  • Workshop bulk push: readiness-check innan task köas

Inställningar — Städning & Omorganisering

  • Borttagna (8 oanvända): claude_model, competitor_alert_threshold_pct, competitor_min_margin_pct, dataforseo_batch_size, dataforseo_monitor_max, shopify_auto_push, shopify_zero_stock_on_grace, shopify_draft_on_expire
  • Ihopslagna flikar (7→4): Allmänt, Priser & Konkurrenter, Shopify & AI, API-nycklar
  • Kopplade till kod: default_grace_period_days → feed_presence, min_description_length → pipeline steg 8

Kodstädning

  • 31MB+ loggfiler borttagna
  • 10 gamla/oanvända scripts borttagna (1619 rader)
  • .gitignore uppdaterad: tmp_*, *.dump, *.csv, *.jsonl
  • Oanvänd asyncio-import borttagen
  • Nytt index: ix_products_competitor_checked
  • Dockerfile: alembic upgrade head körs vid start

Amazon-skraper (experimentell)

  • nexus/scrapers/amazon.py — stödjer amazon.com/.pl/.de
  • curl_cffi med browser impersonation
  • EAN→UPC konvertering, namnbaserad sökning
  • Begränsning: Amazon stödjer inte EAN-sökning via webb

---

v1.3.0 Prisjamforelse, Bildpush & Skraper-forbattringar 2026-03-18

Prisjamforelse — Komplett ombyggnad

  • UI: 5 flikar -> 3 — Priser (produkttabell + filter), Analys (prisjustering + insikter), Installningar (automation + handlare)
  • Automationsbanner: gron/gul/rod statusindikator for Pricerunner-token och bevakning
  • Fraschthetsindikatorer: gron/gul/gra/rod prickar pa varje produktrad
  • Inline row-expansion: klicka rad for snabb vy av konkurrentpriser utan modal
  • Bulk-checkboxar: valj flera produkter -> "Kolla priser" for on-demand priskoll
  • Toast-notiser istallet for alert()
  • localStorage filter-persistens: filter sparas mellan sidladdningar
  • Nytt API endpoint: GET /api/competitors/automation-status

Daglig Pricerunner-automation

  • daily_competitor_rotation Celery task: 1000 produkter/dag, aldst-forst rotation
  • daily_google_shopping_rotation Celery task: 50 produkter/dag (valfritt, betalt)
  • Beat schedule: Pricerunner kl 09:30, Google Shopping kl 10:00
  • 4 nya installningar: competitor_daily_enabled/limit, google_shopping_daily_enabled/limit
  • Full rotation av alla produkter var 3-4 dag

Pricerunner-klient — Fixar

  • get_offers_single() anvander nu batch-endpoint (single-path-endpoint finns inte i Merchant API v1)
  • Rate limit-loggning: detekterar daglig grans (x-ratelimit-day-remaining: 0) och stoppar omedelbart
  • Tydliga loggmeddelanden med remaining/limit-raknare

iHerb-skraper — Forbattringar

  • Bilder: ny metod med button.thumbnail-item img[data-large-img] for korrekt bildordning (bild 1, 2, 3...)
  • Videos: extraherar YouTube-embeds fran produktgalleri
  • Rankings: forbattrad med .best-selling-rank selector + fallbacks
  • Snabbare: delay sankt till 50-150ms, 50 workers default
  • Rescrape: 3880 EANs omskrapade med forbattrad skraper, 4068 uppdaterade i DB
  • Cleanup: gamla fix_iherb_images.py och import_iherb.py borttagna

Shopify bildpush — Ny

  • scripts/push_images_shopify.py — pushar skrapade iHerb-bilder till Shopify
  • Smart bildhantering:
  • Bild 1 per variant = variantens huvudbild (kopplad via image_id)
  • Bilder 2-7 fran forsta varianten = delat galleri (produktniva)
  • Inga duplicates mellan varianter
  • Filtrerar bort iHerb-banners/CMS-bilder automatiskt
  • Parallella workers (default 4) med asyncio.Semaphore
  • Resume-stod: --skip N for att fortsatta fran avbruten korning
  • Resultat: 2346 produkter, 0 misslyckade

Amazon-skraper — Ny (experimentell)

  • nexus/scrapers/amazon.py — stodjer amazon.com, amazon.pl, amazon.de
  • curl_cffi med browser impersonation
  • Parsning av: titel, brand, bilder, rating, ingredienser, specs, rankings
  • EAN->UPC konvertering (13->12 siffror)
  • Namnbaserad sokning som fallback
  • Begransning: Amazon's webbsokning stodjer inte EAN/UPC-sokning — bast for manuellt bruk

---

v1.2.0 Claude Haiku + Bulk Content Generation 2026-03-15

AI-modellbyte: Claude Haiku 4.5

  • Ny default: Alla AI-generering (beskrivningar, ingredienser, FAQ) kör nu på Claude Haiku 4.5
  • Ny call_claude_generation(): Separerad funktion för content generation vs analys
  • Konfigurerbar modell: Inställning ai_generation_model — Haiku (standard) eller Sonnet
  • Jämfördes Sonnet vs Haiku: Haiku 2x snabbare/billigare, acceptabel kvalitet med post-processing

Bulk innehållsgenerering

  • Ingredienser: 19,008 produkter med ny ENRICH_INGREDIENTS-prompt
  • Promptregel 8: inga markdown-wrappers
  • 1,325 "Innehål"-stavfel fixade i DB
  • Källa: 99,9% från feed-data, 0,1% DuckDuckGo EAN-sökning
  • Beskrivningar: 7,630 parents med AI HTML-beskrivningar
  • Retry med enklare prompt om första försöket returnerar tomt
  • Berikad kontext: strukturerade ingredienser, vikt, kategori, EAN-fallback
  • FAQ: 8,606 parents med 5+ FAQ-frågor vardera
  • Kort beskrivning: 8,838 parents (99% täckning)

Pipeline — 20 steg

  • 2 nya steg synliga i UI: Steg 19 (FAQ) och 20 (Användning & Näring) i Enrichment-fasen
  • Batch-commits: Var 300:e produkt under körning (var: bara i slutet)
  • Smart Mode: Stöd för steg 19-20
  • Concurrency: Steg 10 höjt till 150, steg 8 sänkt till 40 (undviker DB-konflikter)
  • run_pipeline.py: ny --limit N flagga för testning

Produktverkstad — Filterfix

  • "Att godkänna" visar nu korrekt bara ogodkända grupper
  • "Godkända" visar bara godkända grupper
  • "Alla" visar allt (visade förut bara ogodkända)
  • all_approved-beräkning fixad: accepterar både "true" och timestamp-värden
  • openGroupById(): använder parent-ID istället för array-index (förhindrar fel grupp)
  • Backend: laddar alla grupper sorterade (ogodkända först)

UI-uppdateringar

  • Pipeline-sidan: "5 faser — 20 steg"
  • Dashboard: pipeline-knappen triggar steg 1-20
  • Inställningar: uppdaterade beskrivningar för 20 steg

---

v1.1.0 Resilient Push, Viktbugg & Celery Beat 2026-03-13

Resilient Push — Pushen blockeras aldrig helt

  • Ny auto_fix_before_push() — auto-fixar saknade fält innan push:
  • Vikt: extraheras från produktnamn via extract_weight_from_name() ("908g" → 0.908 kg)
  • Pris: kör pricing engine om grossistpris finns men säljpris saknas
  • Brand: ärver från parent om variant saknar
  • Ny partition_variants_for_push() — delar upp varianter i pushable/excluded:
  • Utgångna (ingen grossist + inget lager + aldrig pushad) → auto-inaktiveras
  • Har Shopify-närvaro men saknar grossist → flaggas för review
  • Övriga unfixable → exkluderas men behålls aktiva
  • _try_auto_push_group() omskriven — använder partitionering, loggar exkluderingar via activity_log, pushar redo varianter
  • _try_auto_push_standalone() uppdaterad — kör auto-fix innan readiness-check
  • Safety net i enrich_then_push — auto-fix som sista chans innan Phase 2 readiness-recheck
  • 2 nya inställningar: resilient_push_enabled (default: på), auto_inactivate_discontinued (default: på)

Viktbugg — BioU importerade gram som kg

  • Bugg: BioU XML-feed skickar vikt i gram (t.ex. 1880) men import_engine lagrade det direkt som kg
  • Fix: Ny _normalize_weight_kg() i import_engine — om vikt > 50, konvertera g→kg automatiskt
  • Retroaktiv fix: 3 570 produkter korrigerade i databasen
  • 1 produkt (#32867 Rhodiola, 150 000g) med absurd leverantörsdata nollställd

Shopify viktsynk

  • Ny push_all_weights() (safe_sync.py) — lättviktig bulk-push av vikter till Shopify
  • Ny Celery-task push_all_weights (time_limit 7200s)
  • Ny API-endpoint POST /api/workshop/push-all-weights
  • Grupperar per Shopify-produkt, en PUT per produkt, uppdaterar bara varianter med avvikande vikt

Celery Beat fix

  • beat_schedule_filename satt till /tmp/celerybeat-schedule i Celery-config
  • Render's ephemeral filsystem saknade skrivbar default-plats → beat startade aldrig
  • Alla 17 schemalagda tasks körs nu korrekt (import, pipeline, pricing, quality audit, etc.)

Övrigt

  • db_query.py — lade till conn.commit() (UPDATE-satser committades aldrig)
  • FIELD_TO_PIPELINE_STEPweight och brand mappade till "auto_fix" (hanteras av auto_fix istället för pipeline)

---

v1.0.0 Stabil release 2026-03-12

Svensk UI — Komplett översättning

  • Alla texter genom hela appen använder nu korrekta svenska tecken (å, ä, ö)
  • Fixat i: Inställningar, Aktivitetslogg, Mitt konto, Dashboard, Produktverkstad
  • Inställningsnamn och beskrivningar: "Avrundningsläge", "Prisändringsvarning", etc.
  • Aktivitetsetiketter: "Godkände grupp", "Döpte om produkt", "Ändrade inställning", etc.

Inloggning — Ny design

  • Modern split-screen layout — mörk gradient med logga till vänster, formulär till höger
  • Stöd för vita logotyper — syns tydligt mot mörk bakgrund
  • Visa/dölj lösenord — ögon-ikon i lösenordsfältet
  • Laddningsindikator — spinner vid inloggning
  • Mobilanpassad — logga i mörk rundad container på små skärmar

Buggfixar

  • Statistikräknare — "Att godkänna"-räknaren uppdateras korrekt vid batch-godkännande
  • Blocklista — retry-logik (2 försök med 2s fördröjning) löser timeout vid kallstart
  • HTTPS Mixed Content — ProxyHeadersMiddleware säkerställer korrekta URLs bakom Render-proxy
  • Apostrofer i produktnamnNature's Answer visas korrekt (inte Nature\x27s Answer)
  • Parentnamn i Produktverkstad — fullständigt namn visas med radbrytning istället för trunkering
  • Lösenordsåterställning — ny endpoint POST /api/auth/reset-password

---

v0.14.0 Mitt konto, Aktivitetslogg, Changelog & Auto-sync 2026-03-12

Mitt konto

  • Profilsida (/account) — visa och redigera namn, e-post, roll
  • Byt lösenord — krav på nuvarande lösenord för säkerhet
  • Senaste aktivitet — visa egna händelser på kontosidan
  • Klickbart användarnamn i header-baren länkar till Mitt konto

Aktivitetslogg — Spåra vem som gör vad

  • Ny ActivityLog-modell — sparar användare, händelse, objekt, IP-adress, detaljer (JSONB)
  • Instrumenterade endpoints: inloggning, gruppgodkännande, produktändringar, namnändring, inställningsändringar, förslag accept/ignore
  • Admin-vy (/activity) — filtrerbara loggar med paginering
  • Auto-rensning — Celery-task rensar loggar äldre än 90 dagar (konfigurerbart via inställningar)
  • Ny setting: log_retention_days (default 90, 7–365)

Nyheter / Changelog

  • Ny sida (/changelog) — renderar CHANGELOG.md som snygga versionskort
  • Versionslänken i sidebar-footer länkar till changelog
  • Aktuell version markerad med grön badge
  • Minimal Markdown-parser — hanterar rubriker, listor, tabeller, fetstil, kodblock

Auto-sync till Shopify

  • auto_sync_changed_products() — synkar alla ändrade fält (pris, beskrivning, SEO, tags, cost, namn) var 30 min
  • Brand-dash fix i diff-update — "SciTec Marine" → "SciTec - Marine" (samma logik som mapper.py)
  • Radering istället för arkivering — dubbletter raderas vid konsolidering, ingen fallback till arkivering
  • Lager exkluderat — hanteras av externt system

Inställningar

  • Ny Schema-flik — visuell tidslinje med dagliga tasks (06:00–09:00) och periodiska uppgifter
  • Blocklista förbättrad — sökfält + paginering (50 per sida)
  • Daglig pipeline kl 06:30 (steg 1–18, smart mode)
  • AI-förslag kl 09:00
  • Loggrensning kl 03:00

Alembic Migration

  • d4e5f6g7h8i9activity_logs tabell + users.last_login_at kolumn

---

v0.13.0 Workshop UX, Produktdetaljer & Branding 2026-03-11

Produktverkstad — Snabbare & Smartare

  • 10x snabbare grupplistning — ny get_group_list_fast() använder SQL-aggregation istället för ORM eager loading
  • Inga variant-objekt laddas — bara counts och preview-namn via array_agg()
  • Ny endpoint GET /api/variants/list ersätter tung /api/variants/data för listvy
  • Auto-load vid tom workspace — när alla grupper godkänts laddas automatiskt 5 nya ogodkända grupper
  • "Ladda fler grupper"-kort synligt i workspace för manuell laddning
  • Same-brand parallellt arbete — ny GET /api/variants/brand-groups API
  • Klick på "Redigera grupp" laddar huvudgruppen + upp till 5 grupper från samma varumärke
  • Parallell laddning via Promise.all() för snabb öppning
  • Rikare gruppkort — bild, lagerantal, Shopify-status och push-badge
  • Produktbild (12×12) med fallback-placeholder
  • Lagerantal, Shopify-variant count, push-status badge
  • Tydliga knappar: "Redigera grupp" (workspace) vs "Produktsida" (detaljvy)

Produktdetalj — Förbättrad Information

  • Beskrivningsförhandsgranskning fixad — hanterar både HTML och plaintext korrekt
  • HTML renderas via DOMParser, plaintext med radbrytningar och styckeindelning
  • Null guards för produkter utan beskrivning
  • Aktiv leverantörsbar — visar leverantörsnamn, pris (EUR), senaste import, Shopify-butikslänk
  • EAN + Supplier SKU i erbjudandetabellen med klickbara leverantörslänkar (↗)

Branding & UI

  • Konfigurerbar app-logga — ny setting app_logo_url i Inställningar
  • Logga visas i sidebar (h-12) och login-sida (h-20)
  • App-namn + version flyttat till botten av sidebar i ljusgrå
  • Konfigurerbart appnamnapp_name setting, default "Nexus PIM"
  • Template globalsapp_name, app_logo_url, app_version tillgängliga i alla templates

Användarhantering

  • Ny endpoint PUT /api/auth/update-user — admin kan uppdatera e-post, namn och lösenord
  • Rollkontroll — kräver admin-roll för att uppdatera användare

---

v0.12.0 Generated RRP, Weight Pricing, Supplier Auto-Reprice & Produktverkstad 2026-03-11

Genererat Rek.pris (compare_at_price)

  • Ny generate_compare_at_price() (pricing_engine.py) — genererar rekommenderat pris för Shopify
  • 3-stegs prioritet: 1) Leverantörs-RRP om ≥15% över säljpris, 2) Genererat hash-baserat RRP, 3) Inget om hög marginal
  • Deterministisk variation — MD5-hash av produkt-ID ger unik markup per produkt (20-35%), ser naturligt ut
  • Psykologisk avrundning — alla priser slutar på 9 (229, 249, 299, 339...)
  • Marginalspärr — produkter med >55% marginal får inget RRP (redan "dyra" relativt inköp)
  • 3 konfigurerbara settings: rrp_min_markup, rrp_max_markup, rrp_high_margin_cap
  • Integrerat i alla push-flöden: mapper.py, safe_sync.py (update + push_all_prices)

Viktkonditioner i prisregler

  • _check_conditions() stödjer nu weight_min och weight_max (kg)
  • Pricing UI — nya fält "Min vikt" och "Max vikt" i regelmodalen
  • Användning: Skapa fixed_amount-regel med {"weight_min": 3.0} → +50 kr för tunga produkter
  • Visas i regellistan som "≥ 3 kg"

Auto-prisberäkning vid leverantörsbyte

  • update_active_supplier() — kör calculate_price(apply=True) automatiskt vid byte
  • select_all_best_suppliers() — batch-version: samlar ändrade produkter, beräknar om pris
  • Säkerställer att priset alltid matchar aktiv leverantörs regler

BioU RRP-valutafix (PLN → EUR)

  • Upptäckt: BioU:s retailPriceGross är i PLN, inte EUR — verifierat via BioU REST API
  • import_engine.py — ny rrp_currency-parameter, konverterar PLN→EUR vid import
  • xml_feed.py — skickar rrp_currency="PLN" för BioU
  • Ny setting: pln_to_eur_rate (default 0.233) i Inställningar
  • 3 926 BioU-produkter korrigerade retroaktivt

Shopify Variant-ID Synkronisering

  • Ny sync_shopify_variant_ids() (safe_sync.py) — matchar PIM-produkter till Shopify via EAN/barcode
  • Ny Celery-task sync_variant_ids (time_limit 3600s)
  • Resultat: 4 160 av 8 399 produkter matchade och länkade
  • Löser problem med produkter pushade från gamla PIM som saknade variant-ID

Shopify Bulk-prispush

  • Ny push_all_prices() (safe_sync.py) — lättviktig pris-push (price + compare_at + cost)
  • Grupperar per Shopify-produkt — en PUT per produkt istället för per variant
  • Resumableskip_processed-parameter för att fortsätta efter timeout
  • Ny Celery-task med time_limit 7200s + soft_time_limit 7000s
  • API-endpoint: POST /api/workshop/push-all-prices
  • 9 018 priser uppdaterade på Shopify (5 122 produkter)

Produktverkstad — Workshop Förbättringar

  • Exakta DB-stats — ny get_group_stats() med direkta SQL-queries istället för att räkna laddade grupper
  • Ny API: GET /api/variants/stats — snabb stats utan produktladdning
  • 4 statboxar: "Att godkänna", "Godkända", "Totalt grupper", "Totalt varianter"
  • Godkända grupper filtreras bort — visar bara grupper som behöver godkännas
  • AI-förslag tab-count fix — räknar ner från server-total vid accept, inte sidans längd
  • Fulla produktnamn — borttagen truncate CSS, namn visas på egen rad under brand

Integrationer & API

  • Claude API-nyckel — ny setting i Integrationer (med env_fallback)
  • Claude-modell — konfigurerbar dropdown (Haiku/Sonnet/Opus)

Worker-skalning

  • Celery concurrency ökad från 4 till 8 workers (-c 8)

---

v0.11.0 Pricing Engine Port & Shopify Push Fixes 2026-03-10

Pricing Engine — Fullständig port från gamla PIM

  • Ny prismotor (nexus/services/pricing_engine.py) — portad 1:1 från gamla PIM:s beprövade logik
  • Arbetar i SEKbase_price-regel konverterar EUR→SEK först, sedan alla beräkningar i SEK
  • Regelkedja — regler appliceras i prioritetsordning, varje regels output matas till nästa
  • stop_processing-flagga — kan stoppa regelkedjan efter en specifik regel
  • 5 regeltyper: base_price (EUR→SEK), percentage_markup (×faktor), fixed_amount (+belopp), margin, currency_conversion
  • Prisflöde: Inköp EUR → ×11.50 (SEK) → ×1.575 markup → +10 kr om <150 SEK → ×1.12 moms → avrundning uppåt
  • Brand overrides: exkludering (OLY, Hairtamin) och custom margin (margin_multiplier)
  • BioU Yango/Ostrovit: ×1.89 istället för ×1.575 (via brand-specifika regler med conditions)

Nya modellfält (SupplierPricingRule)

  • name (String 200) — regelnamn
  • description (Text) — regelbeskrivning
  • percentage_value (Float) — t.ex. 1.575 för 57.5% markup
  • fixed_amount (Float) — t.ex. 10 kr lågpristillägg
  • currency_from / currency_to (String 10) — valutakonvertering (EUR→SEK)
  • stop_processing (Boolean) — stoppa regelkedjan efter denna regel
  • BrandPricingOverride: nytt fält margin_multiplier (Float)

Shopify Push Fixes

  • Priser i SEK direktcalculated_sell_price_incl lagras nu i SEK (inte EUR), ingen konvertering vid push
  • _sell_price_to_sek() borttagen — heuristiken för EUR/SEK-detektering behövs inte längre
  • mapper.py — förenklad _variant_pricing(), använder pris direkt
  • safe_sync.py — borttagen eur_to_sek()-konvertering vid diff-update och variant-push
  • push_readiness.py — förenklad prisvalidering, ingen EUR/SEK-heuristik

Shopify Content & Metadata

  • Metafield-push via POST — separata POST-anrop per metafield (Shopify REST API ignorerar metafields i PUT)
  • Korrekta namespaces: c_f/ingrediensforteckning, next_cart/short_description
  • Compare-at price — RRP som compare-at, bara om ≥15% högre än sellpris
  • Taxonomi-kategori — statisk mappning via taxonomy_mapping.json (60+ kategorier)
  • Collection-matchning — ordöverlappning istället för substring, min 3 tecken, exkluderar "P"/"All"/"Rabatterade produkter"

Alembic Migration

  • b2c3d4e5f6g7 — alla nya kolumner för pricing engine port

Batch-omprissättning

  • 18 364 produkter omprissatta med nya regler
  • 638 utan inköpspris (förväntat)
  • Verifierade beräkningar matchar gamla PIM exakt

---

v0.10.0 Dashboard Redesign & User-Configurable Settings 2026-03-09

Dashboard Redesign

  • Ny startsida med 4 rader: nyckeltal, pipeline-funnel + feed health, leverantörer + content + kvalitet, snabbnavigering
  • 6 nyckelkort (rad 1): Aktiva produkter, Shopify-pushade, Redo att pusha, Prissatta, Grupper, Leverantörer
  • Shopify pipeline-funnel (rad 2): visuell tratt från Ej pushade → Redo → Pushade med antal per steg
  • Feed health-panel (rad 2): produkter i grace period, försvunna offers, stock=0, prisändringar flaggade
  • Leverantörsstatus (rad 3): aktiva/inaktiva leverantörer, offers per leverantör
  • Content-komplettering (rad 3): beskrivning, SEO, ingredienser med procent
  • Kvalitetsöversikt (rad 3): medelpoäng, issues-fördelning
  • get_product_stats() — utökad från 7 till ~20 nyckeltal

User-Configurable Settings System

  • Ny nexus/services/settings_service.py — central tjänst med SETTING_DEFINITIONS dict
  • 15 inställningar i 5 grupper: Prissättning, Import, Feed & Lager, AI & Pipeline, Shopify
  • Auto-renderad UI — nya inställningar syns automatiskt på settings-sidan bara genom att lägga till i SETTING_DEFINITIONS
  • Stöd för: number (med min/max/step), select (dropdown), toggle (boolean)
  • API: GET /settings/ (lista med definitioner), GET /settings/definitions, PUT /settings/{key}, PUT /settings/ (bulk)
  • Ersätter hårdkodade värden i pricing_engine.py (max_sell_price_sek) och import_engine.py (price bounds)
  • Designprincip: alla nya features exponerar sina viktigaste parametrar som inställningar

Settings-sida Redesign

  • 3 flikar: Konfiguration, Blocklista, Brand Mappings
  • Konfigurationsfliken renderar grupper med färgkodade sektioner automatiskt från SETTING_DEFINITIONS
  • Bulk-sparning — sparar alla ändrade inställningar i ett anrop

---

v0.9.0 Feed Presence Management & Production Hardening 2026-03-09

Feed Presence Management — Ny Service

  • Ny nexus/services/feed_presence.py (~280 rader) — central logik för produkter som försvinner/återkommer
  • deactivate_missing_offers() — deaktiverar offers som inte synts i senaste import
  • Safety check: skippar om import_count < 50% av senaste lyckade (skydd mot partiella feeds)
  • Extra safety: skippar alltid om import_count == 0
  • _handle_offer_loss() — byter till billigaste aktiva leverantör eller startar grace period
  • detect_and_reprice_changed() — auto-omprisa vid små prisändringar (<20%), flagga stora (>20%)
  • check_grace_periods() — daglig task: nolla stock direkt, draft efter grace period
  • reactivate_product() — återaktivera produkt, köa Shopify-återställning

8 Scenarion Hanterade

#ScenarioÅtgärd
11 av N leverantörer tappar produktDeaktivera offer → byt leverantör → omprisa
2Flera (inte alla) tapparCascading offer-förlust
3ALLA tapparGrace period → stock=0 → draft efter grace
4Återkommer under graceAvbryt grace → återaktivera → omprisa
5Återkommer efter deaktiveringÅteraktivera → omprisa → Shopify active (om PUSHED)
6Inprisändring<20%: omprisa, >20%: flagga
7Stock=0 men i feedUppdatera stock → grace i DAGAR
8Partiell/tom feedSafety check → skippa deaktivering

Nya Modellfält

  • Supplier: grace_period_days, min_feed_count_ratio, last_successful_import_count, price_change_alert_pct
  • SupplierOffer: disappeared_at, stock_zero_since, previous_wholesale_euro, price_change_flagged
  • Product: grace_period_started, shopify_stock_zeroed_at, shopify_drafted_at
  • ImportResult: seen_eans: set[str]

Import Engine — Prisändringsdetektering & Stock-spårning

  • import_engine.pyoffer.previous_wholesale_euro sparas INNAN nytt pris sätts
  • Stock-spårning: offer.stock_zero_since = now vid stock → 0, rensas vid stock > 0
  • result.seen_eans.add(ean) — spårar alla EAN som synts i importen
  • Reaktivering rensas: grace_period_started, shopify_stock_zeroed_at

Import Task Integration

  • Fas 1.5deactivate_missing_offers() körs efter import, innan commit
  • Fas 3detect_and_reprice_changed() körs efter best supplier selection
  • Daglig taskdaily-grace-period-check via Celery Beat (kl 08:00)

ShopifyClient — 4 nya metoder

  • set_product_status(product_id, status) — PUT active/draft/archived
  • get_inventory_item_id(variant_id) — GET variant inventory_item_id
  • get_location_id() — GET primary location (cachad)
  • set_inventory_level(inventory_item_id, location_id, quantity) — POST inventory_levels/set

Centraliserat Exception-system

  • Ny nexus/exceptions.pyPIMError (400), NotFoundError (404), ConflictError (409), ShopifyError (502), ValidationError (422)
  • Exception handler i app.py — returnerar JSON {"detail": ...} med rätt statuskod
  • Ersätter inkonsekvent felhantering (ValueError, bare 500:or)

Redis Lock — Fail Closed

  • safe_sync.py_acquire_group_lock() kastar nu redis.ConnectionError istället för att fortsätta utan lås
  • Förhindrar duplicerade Shopify-produkter vid Redis-avbrott

Celery Task Timeouts

  • Alla tasks har nu time_limit: 300s (standard), 600s (batch), 1800s (pipeline), 3600s (full import/pipeline)
  • Förhindrar tasks som hänger i evighet

Säkerhet

  • config.pysecret_key kräver nu env-variabel (ingen osäker default)
  • .gitignore — ny fil, täcker .env, __pycache__, venv, IDE, OS, logs

Databasindex & Migration

  • ix_products_grace — partial index på grace_period_started
  • ix_offers_active_supplier — composite index (supplier_id, is_active)
  • ix_offers_disappeared — partial index på disappeared_at
  • Alembic migration a1b2c3d4e5f6 — alla nya kolumner + index

---

v0.8.0 Description Prompt & Move Conversion 2026-03-08

Description Prompt (portad från gamla PIM)

  • nexus/ai/prompts.pyENRICH_DESCRIPTION: inga generiska mall-exempel, hälsopåståenderegler
  • Minimum 1000 tecken enforced i step_description() — kortare avvisas och genereras om
  • Befintliga beskrivningar < 1000 tecken behandlas som "ej tillräckligt bra" → regenereras
  • Hälsopåståenden: NEJ till "botar/behandlar/förebygger", NEJ till "anti-aging/anti-inflammatorisk"
  • OK: EU-godkända påståenden som "bidrar till normal funktion av immunsystemet"

Move Parent-to-Variant Conversion (portad från gamla PIM)

  • move_variants() — beräknar variant_name via strip_parent_from_name() vid flytt
  • Rensar ärvd description (om inte manuellt redigerad) när produkt blir variant
  • Barn omparenteras till target med loggning

---

v0.7.1 Column Search Filter & Rename Edge Case Fix 2026-03-07

Column Search Filters Out Parent Products

  • search_products() — skips parent products without EAN that have active children
  • Parents are group headers, not draggable variants — they should never appear in per-column search results
  • Fetches 2x limit to compensate for filtered-out results

Rename Fix for Nested Parents (Both Child and Parent)

  • do_action("rename") — checks has_children before deciding what to update
  • Pure variants (child, no children): update variant_name only
  • Products with children (column headers): always update name
  • Products that are both child AND parent: update both name and variant_name
  • Fixes bug where renaming a column header that was also a child only updated variant_name, making it look like nothing changed

---

v0.7.0 Stale ID Validation, Auto-Link at Approve, Move Re-Parent & VO Fixes 2026-03-07

Stale Shopify ID Validation in push_group

  • _push_group_inner() — validates all Shopify product IDs via GET before using them
  • Stale IDs (404 / unreachable) are cleared from parent + variants automatically
  • _clear_stale_shopify_id() — helper that resets shopify_product_id, variant_id, handle and push_status
  • Prevents "Invalid Product" errors when PIM references deleted Shopify products

Approve Auto-Links Unlinked Variants

  • approve_group() accepts optional variant_ids parameter
  • Variants shown in VO workspace but not yet DB-linked are auto-linked to parent before approval
  • ApproveRequest schema updated with variant_ids: list[int] | None
  • Frontend sends all workspace variant IDs in approve request

Move Re-Parents Children

  • move_variants() — when moving a product that has children, re-parents children to target instead of leaving them orphaned
  • Prevents broken parent chains when reorganizing groups

Rename Updates variant_name for Child Products

  • do_action() rename — if product has parent_product_id, updates variant_name (not name)
  • Protects variant_name via ManualEditProtection
  • Parent products still update name as before

VO Workspace Cache Validation

  • validateWorkspaceCache() — on page load, validates each cached group exists via API
  • Stale groups (deleted/deactivated parents) are silently removed from workspace
  • Remaining groups are refreshed with latest DB data

VO Filtered List Index Fix

  • renderGroupsList() — uses allGroups.indexOf(group) instead of filtered array index
  • Fixes wrong group opening when clicking items in a filtered list view

---

v0.6.0 Handle Fix, Delete vs Archive, VO UX Improvements 2026-03-06

Handle Fix After Group Push

  • _create_group_product() — kollar om Shopify auto-genererade fel handle (t.ex. -1 suffix från arkiverad kollision)
  • Retry med delay (3 försök, 2s mellan), redirect från fel handle till rätt
  • Löser problem med handles som now-foods-cod-liver-oil-2 istället för now-foods-flax-oil

Delete Instead of Archive in Consolidation

  • _consolidate_group() — raderar nu gamla Shopify-produkter istället för att arkivera
  • Frigör handles omedelbart → inga -1/-2 suffix på nya produkter
  • Fallback till arkivering om radering misslyckas

VO UX — Info Button & EAN Layout

  • Info-knapp (ℹ) alltid synlig bredvid leverantörslänk och rename (inte längre i ⋮-menyn)
  • EAN-kod visas på egen rad under ikonerna i monospace — ger mer plats för variantnamn
  • Drag-and-drop ghost fix — varianten tas bort från källgruppens data omedelbart vid flytt

---

v0.5.0 Duplicate Prevention, EAN Conflict Check & Per-Parent Locking 2026-03-06

EAN Conflict Check at VO Approve

  • _check_ean_conflicts() — kontrollerar om varianters EAN redan finns på andra Shopify-produkter innan godkännande
  • Blockerar approve med detaljerad info om vilka varianter som krockar och vilken Shopify-produkt de pekar på
  • force=True-parameter — användaren kan tvinga godkännande, rensar gamla Shopify-länkar först
  • Frontend confirm-dialog — visar konflikter med EAN + produktnamn, frågar om force

Duplicate Variant Name Dedup

  • group_to_shopify_payload() — detekterar duplicerade variantnamn och lägger till [EAN]-suffix
  • Shopify avvisar varianter med samma namn — denna fix hanterar t.ex. "Hazelnut - 908g" som finns i flera storlekar

Per-Parent Redis Lock

  • _acquire_group_lock() / _release_group_lock() — Redis-baserat lås per parent_id (TTL 120s)
  • push_group() — tar lås innan push, hoppar över om lås redan finns (parallell push pågår)
  • Förhindrar att parallella Celery-tasks skapar duplicerade Shopify-produkter
  • Fail-open: om Redis är nere, fortsätter utan lås

Fix: "All Variants on Same Shopify Product" Case

  • push_group() — nytt scenario: alla varianter pekar på samma Shopify-produkt men parent saknar shopify_product_id
  • Länkar parent istället för att skapa ny Shopify-produkt (undviker dubblett)

---

v0.4.0 Shopify Consolidation, Re-Approval Logic & VO UX 2026-03-06

Re-Approval Logic (smart content preservation)

  • _has_good_content() — kollar om parent redan har bra AI-genererat content (description_is_ai + description + short_description + seo_title)
  • _clear_stale_content() skippar nu rensning om parent redan har bra content — hanterar "lägg till ny variant i befintlig godkänd grupp" utan att radera befintligt content
  • force=True-parameter — tvinga rensning vid strukturella ändringar

Shopify Group Push (batch — 1 API-anrop)

  • _create_group_product() — skapar 1 Shopify-produkt med ALLA varianter i ETT POST-anrop (istället för N separata)
  • group_to_shopify_payload() i mapper.py — bygger payload med alla varianter, options, SEO, ingredienser
  • _detect_option_name() — auto-detekterar "Smak", "Storlek" eller "Variant" från variantnamn
  • _upload_variant_images() — laddar upp bilder post-creation kopplade till specifika Shopify-varianter via variant_ids
  • push_group() hanterar 3 scenarion: ny batch-push, lägg till variant, konsolidering

Shopify Group Consolidation

  • _consolidate_group() — upptäcker varianter spridda på flera Shopify-produkter och konsoliderar till EN

1. Samlar gamla handles för redirects

2. Skapar ny grupperad produkt (batch, alla varianter i 1 anrop)

3. Arkiverar gamla Shopify-produkter

4. Skapar 301-redirects från gamla handles till ny

5. Fixar handle med retry + delay (Shopify behöver tid att frigöra handle)

6. Redirect från auto-genererad fel-handle till korrekt handle

  • Nya ShopifyClient-metoder: archive_product(), create_redirect(), update_product_handle(), delete()

Auto-Push Bugfixes (from old PIM)

  • _try_auto_push_group() använde push_products (individuell push) istället för push_group → skapade separata Shopify-produkter istället för en grupperad
  • Ny Celery-task push_group_task — dedikerad task för grupp-push, anropas av _try_auto_push_group()
  • enrich_then_push Phase 0 — respekterar nu _has_good_content(), skippar content-rensning vid re-approval
  • enrich_then_push Phase 2 — återställer APPROVED-status före push (push readiness recalc kunde sätta READY istället)

VO Design — Quick Actions

  • Leverantörslänk (↗) och redigera (✏) alltid synliga — inte längre dolda i kebab-menyn
  • Sökresultat stannar öppna — vid sökning kan användaren lägga till flera grupper utan att sökresultaten stängs
  • Tillagda grupper markeras med ✓ och gråas ut i sökresultaten

---

v0.3.0 VO Auto-Push, Content Enrichment & Shopify Duplicate Prevention 2026-03-06

VO Approve → Enrich → Push (automatiskt flöde)

  • approve_group() rensar nu gammal content (beskrivning, ingredienser, SEO, tags, handle) på parent innan push
  • Ny _clear_stale_content() — rensar content-fält utan att röra namn/gruppering (skyddat av ManualEditProtection)
  • _try_auto_push_group() och _try_auto_push_standalone() triggar nu enrich_then_push Celery-task om content saknas, istället för att blockera
  • Ny Celery-task enrich_then_push (shopify_tasks.py):

1. Kör enrichment pipeline (steg 8-15: beskrivning, ingredienser, SEO, kategori, pris)

2. Räknar om push readiness

3. Pushar till Shopify

  • approve_variants() gör samma sak för enskilda varianter och standalone-produkter
  • FIELD_TO_PIPELINE_STEP-mappning (push_readiness.py) används för att bestämma vilka pipeline-steg som behövs

Shopify Duplicate Prevention

  • Ny _find_family_shopify_id() — kollar parent + syskons shopify_product_id innan ny Shopify-produkt skapas
  • Ny _add_variant_to_shopify_product() — lägger till PIM-produkt som variant på befintlig Shopify-produkt (via REST API)
  • Stale cache retry — vid tomt API-svar: rensa cache + försök igen
  • push_group() inkluderar "added_variant" i success-count

is_variant-bugg på parents

  • Parents med is_variant=True — produkter som promotas till parent via VO drag-and-drop behöll is_variant=True, vilket fick enrichment-steg 8-14 att skippa dem
  • Fix i link_variant() och move_variants() — sätter target.is_variant = False vid länkning
  • Fix i approve_group() — sätter parent.is_variant = False vid godkännande
  • Ny _fix_variant_flag_on_parents() (pipeline.py) — hittar och fixar parents med is_variant=True automatiskt vid pipeline-start

Nested Parent Cleanup

  • Ny _fix_nested_parents() (pipeline.py) — hittar produkter som har barn men även egen parent_product_id (dubbelroll) och bryter parent-länken
  • Körs automatiskt i pipeline-start via _fix_orphan_parents()

VO Design

  • Actions visas vid hover — knappar (detaljer, byt namn, ny grupp, avlänka, avaktivera) dolda som standard, visas vid hover
  • Mer plats för variantnamn — actions flyttade från vc-top till egen rad under kortet

---

v0.2.0 Pipeline Pause/Resume, Prisskydd & Kvalitetshöjning 2026-03-05

Pipeline Pause & Resume

  • Ny modell PipelineRun (nexus/db/models/task.py) — persistent körningshistorik med status, checkpoint och progress
  • Pause/resume/cancel — Redis pause-flagga + DB-checkpoint gör att pipeline kan pausas mellan steg och återupptas
  • Nya API-endpoints: POST /pause/{run_id}, POST /resume/{run_id}, POST /cancel/{run_id}, GET /runs
  • Import pause — samma mekanism för import-körningar (pausar mellan leverantörer)
  • UI: Paus/Återuppta/Avbryt-knappar i pipeline-sidan, körningshistorik-tabell
  • Page refresh bevarar statecheckActiveRun() återställer progress och polling vid sidladdning

Prisskydd (tre nivåer)

  • Import (import_engine.py):
  • wholesale ≤ 0 → produkt avvisas
  • wholesale < 0.50 EUR → pris stripps (produkt behålls)
  • wholesale > 500 EUR → pris stripps
  • RRP > 2 000 EUR → stripps
  • Pricing engine (pricing_engine.py):
  • Beräknat pris > 15 000 SEK → blockerar med felmeddelande
  • Beräknat pris < 20 SEK → loggar varning
  • Push readiness (push_readiness.py):
  • Saknat/noll sell price → price_no_sell blockerar push
  • Saknat/noll wholesale → price_no_wholesale blockerar push
  • Sell price < 20 SEK → price_too_low
  • Sell price > 15 000 SEK → price_too_high
  • Quality audit (quality_audit.py):
  • 5 nya priskontroller: price_absurd_wholesale, price_suspicious_low_wholesale, price_absurd_sell_high, price_absurd_sell_low, price_absurd_rrp

Beskrivningskvalitet & AI-flagga

  • Nya produktfält: description_is_ai (boolean), description_rating (0-100)
  • Steg 8 sätter description_is_ai = True när AI-beskrivning genereras
  • _rate_description() — poängsätter beskrivningar (0-100) baserat på AI-status, sektioner, ordantal
  • Push readiness kräver description_is_ai = True för att tillåta push
  • Quality audit returnerar 3-tuple: (issues, score, description_rating)

Variant-hantering

  • Import: varianter får aldrig egen description (skippas vid import)
  • Steg 5 (gruppering): rensar description + short_description när produkt blir variant
  • Quality audit: flaggar varianter som har egen description (variant_has_description)

Quality Audit — utökade kontroller

  • 18 nya BAD_PATTERNS (tom HTML, platshållare, AI-läckage på engelska)
  • 13 nya JUNK_VARIANT_NAMES
  • Nya kontroller: desc_not_ai, desc_ai_incomplete, desc_is_name, desc_empty_html, desc_sku_dump, vn_is_ean, vn_is_url, vn_too_long, vn_bad_pattern

Migreringar

  • scripts/create_pipeline_runs_table.sql — PipelineRun-tabell
  • scripts/add_description_fields.sql — description_is_ai + description_rating kolumner

---

v0.1.0 Initial Build 2026-03-05

Komplett nybygge av PIM-systemet. Ersätter gamla PIM (290,000 rader, 668 filer) med modern arkitektur.

Tech Stack

  • Backend: FastAPI (async) → ersätter Flask
  • Databas: PostgreSQL 16 (asyncpg) → ersätter SQLite
  • ORM: SQLAlchemy 2.0 (async mapped_column) → ersätter SQLAlchemy 1.x
  • Migrering: Alembic → ersätter manuella ALTER TABLE
  • Jobbkö: Celery + Redis → ersätter threading + globala dicts
  • Cache: Redis → ersätter filbaserad JSON-cache
  • Frontend: HTMX + Jinja2 + Tailwind CSS → ersätter inline JS/CSS + jQuery
  • AI: GPT-4o-mini (samma)
  • Auth: JWT + bcrypt → ersätter ingen auth
  • Deploy: Docker Compose → ersätter lokal python app.py

Struktur (117 filer, ~6,700 rader)

  • nexus/db/models/ — 10 modeller (Product med 45 kolumner, JSONB manual-edit)
  • nexus/api/ — 12 API-routers (max 250 rader/fil)
  • nexus/services/ — 8 tjänster (pricing, quality, push_readiness, etc.)
  • nexus/importers/ — 5 filer (base, field_mapping, import_engine, powerbody, xml_feed)
  • nexus/ai/ — 10 filer (client, prompts, pipeline, smart_naming, 6 steg-filer)
  • nexus/shopify/ — 7 filer (auth, client, cache, mapper, safe_sync, diff, backfill)
  • nexus/tasks/ — 6 Celery-tasks (import, pipeline, pricing, shopify, quality, scheduled)
  • nexus/utils/ — 5 verktyg (ean, currency, text, auto_grouping, pagination)
  • nexus/templates/ — 10 templates (base + Tailwind sidebar + HTMX)
  • scripts/ — 2 (migrate_from_legacy, run_pipeline)

Nyckelförbättringar vs gamla PIM

ProblemLösning
app.py = 7,468 rader, 118 routes12 router-filer, max 250 rader/fil
26 boolean *_manually_editedEN JSONB-kolumn manually_edited_fields
EAN-normalisering på 3 ställenutils/ean.py (1 ställe)
SupplierOffer upsert på 3 ställenimport_engine.py (1 ställe)
_call_ai() på 3 ställenai/client.py (1 ställe)
338 inline script-blockapp.js + HTMX
Ingen authJWT + bcrypt
SQLite concurrency-problemPostgreSQL
Bakgrundstrådar + globala dictsCelery + Redis
Ingen migreringAlembic
debug=True i produktionPydantic Settings + .env

Datamodell

  • Product: ~45 kolumner (ned från ~95), JSONB manually_edited_fields, PushStatus enum
  • SupplierOffer: 1 EAN = 1 Product, N offers (UNIQUE product_id+supplier_id)
  • User: JWT auth med email, bcrypt-hash, roller (admin/viewer)
  • Nya index: parent_product_id, push_status, brand, active_supplier_id
  • CheckConstraint: id != parent_product_id

Pipeline (18 steg)

1-4: Namnhantering (normalisera, bracket, brand, variant-extraktion)

5-7: Gruppering (AI auto-gruppering, variant-namngivning, Shopify-länkning)

8-14: AI-berikning (beskrivning, ingredienser, SEO, taggar, kategorisering)

15: Auto-prissättning

16-17: Namnkomplettering + titel-normalisering

18: Kvalitetsgranskning (36+ regelbaserade kontroller)

Events

  • product.created → pipeline steg 1-7 (Celery)
  • product.price_changed → auto-prisberäkning
  • product.enriched → push readiness check
  • product.push_ready → auto-push om approved

Kommandon

  • docker compose up — starta alla containers
  • make migrate — kör Alembic-migrering
  • make test — kör tester
  • python -m scripts.migrate_from_legacy — migrera från gamla PIM
  • python -m scripts.run_pipeline --steps 1-18 — kör pipeline