Nyheter & Uppdateringar
v1.0.0Svensk 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