Nyheter & Uppdateringar
v6.3.032 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_productsfiltrerade 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)skipparupdated_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-syncför one-shot-trigger _diff_update_productstämplar nuauto_pushed_atpå både parent och varianter efter varje lyckad diff-sync (inkl. no-op) → incremental-filtretupdated_at > auto_pushed_atfungerar 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_brandinnan 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_readinessistället för rawhas_*-flaggor → stämmer med backendens push-status push_missing_fieldsvisar 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
positionoch 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_jsoni jsonb_array_length (krashade på ogiltig data) pricing_engine.calculate_pricefallbackar till globala regler näractive_supplier_id IS NULLmenwholesale_price_euro > 0→ produkter utan leverantör får ändå prisname_completionhar 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-suppliersquality-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
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 bildenrich_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 meddedup_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=0omkollas 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
---
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_vendorsCelery-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/articleItems→inDate) - 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
---
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-standaloneendpoint: 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-repairendpoint: aterstallde 287 push-status, rensade 61 grace periods- 31 produkter med lager aterstallda fran draft pa Shopify
shopify-healthdiagnostik-endpoint (grace period, drafted, stock-zeroed)shopify-affected-24hendpoint med konfigurerbar tidsperiodmatch-eansendpoint 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_groupreturnerar detaljerad dict set-shell-parents-ready: satter nya grupper till READY for granskningunapprove-shell-groups: rensar approval sa nya grupper syns i Workshop
Pipeline & Infrastruktur
forceparameter fixad i pipeline API (var tyst bortkastad)- Grace period-check AVSTANGD (orsakade Google Merchant-forsvinnanden)
undo-group-standaloneendpoint for att anga grupperingbulk-delete-productsendpoint 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
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_tasksej registrerad —cleanup_orphan_productsschemalagd 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
formVARCHAR(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_ratiohojd 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_dayssatt 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 Beatrestore_shopify_stock— Aterstaller Shopify-lager fran PIM stock, Redis resumeauto_sync_shopifyuppgraderad: 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_overlapfixad:max()istfmin()i namnare — korta parent-namn far inte langre artificiellt hog overlapDECIDE_GROUPINGprompt 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
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_authvaliderar 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-openoch/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.pykompileras 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 medWHERE 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.pylä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_productanvänder nugenerate_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ördatetime.now(naiva timestamps i timezone-kolumn) - Hårdkodade rates (4 ställen): VAT
1.12och EUR/SEK11.5i 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_lockkörde synkront Redis i async-kontext — nu viaasyncio.to_thread() - Duplicate push:
_assign_product_categoryanropades dubbelt ipush_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
appuseristället för root - Render SECRET_KEY: Web och worker delar nu samma nyckel (var separata
generateValue) - Celery retry:
push_products,push_group_taskochenrich_then_pushhar nuautoretry_for,retry_backoffochmax_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.pyochpyproject.tomlanvänder samma version .env.examplekomplett: 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-cleanupvariant_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 iapp.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 tillconsole.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.py—build_shopify_titleedge 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.cssochpackage.jsonskapade- Kör
npm install && npm run css:buildför att generera kompilerad CSS (ersätter CDN)
---
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_idvia 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örTrue) 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:
cbdochhemp oilsom 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_idinnan markering - ShopifyClient skapas före title-check i
_push_group_inner - Tomma varianter (utan EAN) auto-deaktiveras vid merge
---
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_atdetekterar osyncade ändringar_diff_update_productjä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önPOST /api/pipeline/revoke/{task_id}— döda specifik taskGET /api/pipeline/queue-status— visa aktiva/köade tasksGET /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)
---
KRITISK: Inköpspris-bugg fixad (BioU + Naturamed)
priceAfterDiscountNeti 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-bulkendpoint — 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,vcaps→Vcaps,Tabl.→Tabsetc.- 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_productmarkerar bara varianter medshopify_variant_idsom 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_pushochpush_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_excludedboolean på products
Buggfixar
run_pipeline()saknandeonly_new-parameter →NameErrorienrich_then_push- Step 11 pre-filter exkluderade produkter med collection men utan SEO-titel
enrich_then_pushPhase 0 rensade intestructured_ingredientsför standalonepush_groupblockerade på saknade ingredienser → nu bara warning
---
AI-assistent
- Claude-baserad AI-chat med affärsintelligensverktyg
- 6 verktyg: produktsökning, statistik, prisanalys, konkurrentdata, lagerdata, pipeline-status
---
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_quantitypå 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 + antalGET /api/wms/stock— paginerad lagerlistaGET /api/wms/ghost-products— saknade produkterGET /api/wms/slow-moving— långsamrörligt lagerPOST /api/wms/sync— manuell synkPOST /api/wms/test-connection— testa anslutning
Inställningar
wms_enabled(toggle),wms_base_url,wms_username,wms_password,wms_goods_owner_id
---
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:
notificationstabell - 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_dayssetting (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
---
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_conditionper 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_ingredientsföris_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+warningssom 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_videosmetafield
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-rankselector + 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 utanstructured_ingredientspush_group: blockerar grupp om ingen variant har ingredienserenrich_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)
.gitignoreuppdaterad: tmp_*, *.dump, *.csv, *.jsonl- Oanvänd
asyncio-import borttagen - Nytt index:
ix_products_competitor_checked - Dockerfile:
alembic upgrade headkö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
---
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_rotationCelery task: 1000 produkter/dag, aldst-forst rotationdaily_google_shopping_rotationCelery 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-rankselector + 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.pyochimport_iherb.pyborttagna
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 Nfor 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
---
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 Nflagga 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ärdenopenGroupById(): 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
---
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.
) men import_engine lagrade det direkt som kg1880 - 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_filenamesatt till/tmp/celerybeat-schedulei 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 tillconn.commit()(UPDATE-satser committades aldrig)FIELD_TO_PIPELINE_STEP—weightochbrandmappade till"auto_fix"(hanteras av auto_fix istället för pipeline)
---
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 produktnamn —
Nature's Answervisas korrekt (inteNature\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
---
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
d4e5f6g7h8i9—activity_logstabell +users.last_login_atkolumn
---
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/listersätter tung/api/variants/datafö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-groupsAPI - 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_urli Inställningar - Logga visas i sidebar (h-12) och login-sida (h-20)
- App-namn + version flyttat till botten av sidebar i ljusgrå
- Konfigurerbart appnamn —
app_namesetting, default "Nexus PIM" - Template globals —
app_name,app_logo_url,app_versiontillgä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
---
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 nuweight_minochweight_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örcalculate_price(apply=True)automatiskt vid byteselect_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— nyrrp_currency-parameter, konverterar PLN→EUR vid importxml_feed.py— skickarrrp_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
- Resumable —
skip_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
truncateCSS, 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)
---
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 SEK —
base_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) — regelnamndescription(Text) — regelbeskrivningpercentage_value(Float) — t.ex. 1.575 för 57.5% markupfixed_amount(Float) — t.ex. 10 kr lågpristilläggcurrency_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 direkt —
calculated_sell_price_incllagras 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
---
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 medSETTING_DEFINITIONSdict - 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) ochimport_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
---
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 perioddetect_and_reprice_changed()— auto-omprisa vid små prisändringar (<20%), flagga stora (>20%)check_grace_periods()— daglig task: nolla stock direkt, draft efter grace periodreactivate_product()— återaktivera produkt, köa Shopify-återställning
8 Scenarion Hanterade
| # | Scenario | Åtgärd |
| 1 | 1 av N leverantörer tappar produkt | Deaktivera offer → byt leverantör → omprisa |
| 2 | Flera (inte alla) tappar | Cascading offer-förlust |
| 3 | ALLA tappar | Grace period → stock=0 → draft efter grace |
| 4 | Återkommer under grace | Avbryt grace → återaktivera → omprisa |
| 5 | Återkommer efter deaktivering | Återaktivera → omprisa → Shopify active (om PUSHED) |
| 6 | Inprisändring | <20%: omprisa, >20%: flagga |
| 7 | Stock=0 men i feed | Uppdatera stock → grace i DAGAR |
| 8 | Partiell/tom feed | Safety 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.py—offer.previous_wholesale_eurosparas INNAN nytt pris sätts- Stock-spårning:
offer.stock_zero_since = nowvid 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.5 —
deactivate_missing_offers()körs efter import, innan commit - Fas 3 —
detect_and_reprice_changed()körs efter best supplier selection - Daglig task —
daily-grace-period-checkvia Celery Beat (kl 08:00)
ShopifyClient — 4 nya metoder
set_product_status(product_id, status)— PUT active/draft/archivedget_inventory_item_id(variant_id)— GET variant inventory_item_idget_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.py—PIMError(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 nuredis.ConnectionErroristä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.py—secret_keykrä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_startedix_offers_active_supplier— composite index(supplier_id, is_active)ix_offers_disappeared— partial index pådisappeared_at- Alembic migration
a1b2c3d4e5f6— alla nya kolumner + index
---
Description Prompt (portad från gamla PIM)
nexus/ai/prompts.py—ENRICH_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äknarvariant_nameviastrip_parent_from_name()vid flytt- Rensar ärvd description (om inte manuellt redigerad) när produkt blir variant
- Barn omparenteras till target med loggning
---
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")— checkshas_childrenbefore deciding what to update- Pure variants (child, no children): update
variant_nameonly - Products with children (column headers): always update
name - Products that are both child AND parent: update both
nameandvariant_name - Fixes bug where renaming a column header that was also a child only updated
variant_name, making it look like nothing changed
---
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 optionalvariant_idsparameter- Variants shown in VO workspace but not yet DB-linked are auto-linked to parent before approval
ApproveRequestschema updated withvariant_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 hasparent_product_id, updatesvariant_name(notname)- Protects
variant_namevia ManualEditProtection - Parent products still update
nameas 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()— usesallGroups.indexOf(group)instead of filtered array index- Fixes wrong group opening when clicking items in a filtered list view
---
Handle Fix After Group Push
_create_group_product()— kollar om Shopify auto-genererade fel handle (t.ex.-1suffix 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-2istället förnow-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/-2suffix 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
---
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 saknarshopify_product_id- Länkar parent istället för att skapa ny Shopify-produkt (undviker dubblett)
---
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 contentforce=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 viavariant_idspush_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ändepush_products(individuell push) istället förpush_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_pushPhase 0 — respekterar nu_has_good_content(), skippar content-rensning vid re-approvalenrich_then_pushPhase 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
---
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 nuenrich_then_pushCelery-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 + syskonsshopify_product_idinnan 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öllis_variant=True, vilket fick enrichment-steg 8-14 att skippa dem - Fix i
link_variant()ochmove_variants()— sättertarget.is_variant = Falsevid länkning - Fix i
approve_group()— sätterparent.is_variant = Falsevid godkännande - Ny
_fix_variant_flag_on_parents()(pipeline.py) — hittar och fixar parents medis_variant=Trueautomatiskt vid pipeline-start
Nested Parent Cleanup
- Ny
_fix_nested_parents()(pipeline.py) — hittar produkter som har barn men även egenparent_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-toptill egen rad under kortet
---
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 state —
checkActiveRun()återställer progress och polling vid sidladdning
Prisskydd (tre nivåer)
- Import (
import_engine.py): wholesale ≤ 0→ produkt avvisaswholesale < 0.50 EUR→ pris stripps (produkt behålls)wholesale > 500 EUR→ pris strippsRRP > 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_sellblockerar push - Saknat/noll wholesale →
price_no_wholesaleblockerar 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 = Truenä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 = Truefö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-tabellscripts/add_description_fields.sql— description_is_ai + description_rating kolumner
---
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
| Problem | Lösning |
| app.py = 7,468 rader, 118 routes | 12 router-filer, max 250 rader/fil |
26 boolean *_manually_edited | EN JSONB-kolumn manually_edited_fields |
| EAN-normalisering på 3 ställen | utils/ean.py (1 ställe) |
| SupplierOffer upsert på 3 ställen | import_engine.py (1 ställe) |
_call_ai() på 3 ställen | ai/client.py (1 ställe) |
| 338 inline script-block | app.js + HTMX |
| Ingen auth | JWT + bcrypt |
| SQLite concurrency-problem | PostgreSQL |
| Bakgrundstrådar + globala dicts | Celery + Redis |
| Ingen migrering | Alembic |
| debug=True i produktion | Pydantic 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äkningproduct.enriched→ push readiness checkproduct.push_ready→ auto-push om approved
Kommandon
docker compose up— starta alla containersmake migrate— kör Alembic-migreringmake test— kör testerpython -m scripts.migrate_from_legacy— migrera från gamla PIMpython -m scripts.run_pipeline --steps 1-18— kör pipeline