Synchronizace skladu
Shoptet ↔ BrickLink

Sync engine + admin platforma · Freelance

Distribuovaný sync engine mezi Shoptetem (český zákaznický e-shop) a BrickLinkem (mezinárodní B2B marketplace pro LEGO). Bez něj by každý díl mohl kolidovat mezi dvěma prodejními kanály — s ním sklad drží, i když majitel spí. Kolem sync jádra postupně vyrostla i admin platforma, která z původního skriptu udělala nástroj pro každodenní provoz e-shopu.

Role Freelance · Sólo full-stack vývoj
Typ Produkční systém
Období Listopad 2025 — současnost
Technologie
Python 3.12 Flask Gunicorn SQLite Docker Shoptet API BrickLink API (OAuth1) Resend API Vanilla JS Webhooky + polling Magic link auth RBAC

Minifigs.cz prodává unikátní LEGO dílky a minifigurky. Zákazníky obsluhuje Shoptet — přehledný český e-shop. Sklad doplňuje nákupy z BrickLinku — nejlepší mezinárodní zdroj LEGO dílů. Dva systémy, jeden sklad. A mezi nimi majitel s tabulkou, kterou musí ručně srovnávat.

Každý překlep znamenal overselling. Každé zapomenuté zapsání znamenalo díl chybějící v už odeslané objednávce. Nebyl to problém jednorázový — vznikal každý den znovu, s každou objednávkou z obou kanálů. Cíl projektu byl na papíře jednoduchý: obě platformy musí souhlasit bez lidské asistence.

Realita byla komplikovanější. Shoptet a BrickLink mají různé identifikátory dílů, různé názvy barev, různé pravidla pro storefront. Sync bez mapping systému by lil objednávky do zdi. Nejdřív se musel postavit slovník — pak teprve překlad.

Sync engine je Python + Flask + Gunicorn v Dockeru, perzistence v SQLite. Shoptet informuje o změnách přes webhooky (instantně, order:create/update/delete). BrickLink webhooky pro inventář nepodporuje, tak se dohání polováním každé 3 minuty. Kolem jádra vyrostla admin platforma jako druhá vrstva — 17 šablon, 13 JS souborů, žádný framework a žádný build krok, protože platforma má jednoho maintainera.

Bezpečnostní vrstva vede přes magic link (bez hesel, token přes Resend API) a RBAC s rolemi admin (plný přístup) a picker (jen picking obrazovky). Bootstrap adminů z ADMIN_EMAILS env proměnné — never-lockout safeguard.

Projekt nezačal jako „postavme admin platformu". Začal jako sync skript. Každá další vrstva přišla jako odpověď na konkrétní bolest — a všechny milníky mají svůj commit v gitu.

Listopad 2025
První verze byla skromná. CLI skript, který spouštěl synchronizaci ručně z terminálu. Sync engine fungoval, ale musel se pouštět.
Prosinec 2025
Druhý problém: 24/7. Sync musí běžet i v noci. Dockerfile, Gunicorn, webhook receiver pro Shoptet. Provoz se odlepil od terminálu.
Únor 2026
Třetí problém: co se právě stalo? Majitel potřeboval vidět, co sync engine dělá — jaké objednávky prošly, kde selhaly, jak vypadá inventář. Vznikly první web routes v Flask blueprintu. Sync engine přestal být čistě backend nástroj.
Květen 2026
Čtvrtý problém: kdo má přístup? Admin s bezpečnostními implikacemi potřebuje auth. Magic link místo hesel, RBAC s rolemi admin/picker. Ve stejném měsíci in-app feedback systém, aby se hlásily bugy tam, kde jsou vidět.
Červen 2026
Pátý problém: picking je pomalý. Picking workflow s reálnými obrázky ze Shoptet CDN, detekce orphan objednávek po deploy/OOM (30min okno pro auto-resume), polish napříč moduly.

Výsledek: ze sync skriptu se stala platforma, kterou majitel používá v každodenním provozu. Bez tlaku na předem navržený monolit — každá vrstva přibyla, teprve když byla evidentně potřeba.

Řešení, na kterých mi projekt nejvíc zabral hlavu a která by se dala použít v jiných distribuovaných systémech.

1. Orphan recovery po deploy / OOM kill

Problém: Když proces umře uprostřed zpracování objednávky (deploy restart, out-of-memory na Railway), zůstanou „in-progress" záznamy v JSON stavu. Bez řešení by jeden špatný deploy znamenal, že se ručně dohledává, co se v okamžiku pádu rozsynchronizovalo.

Řešení: Startup catchup identifikuje stale in-progress orders z mrtvých procesů. Podle stáří je buď automaticky obnoví (window 30 min podle ORPHAN_AUTO_RESUME_WINDOW_MINUTES), nebo označí jako vyžadující manuální zásah. Bez tohohle safeguard by systém nezvládl běžné Railway platform events.

2. Composite items (lightsabery)

Problém: Většina LEGO dílků je 1:1 — jeden Shoptet SKU = jeden BrickLink item. Lightsabery se ale skládají z rukojeti + čepele různých barev. Tracking klíč pro tyhle případy nemůže být jen order_id, jinak by se dvě různé kombinace rukojeť + čepel považovaly za duplikát.

Řešení: Vlastní tabulka composite_items pro Shoptet varianty složené z více BrickLink dílů. Tracking klíč je složený: ean_partno_bladecolor. Proti souběžnému zápisu do stejného item pomáhají per-item locks v BrickLink klientu.

3. Rate limits + resumable mapping generator

Problém: Generování prvních 11 000 mapování znamenalo projít celý BrickLink inventář a spárovat ho s katalogem na Shoptetu. BrickLink API má rate limity, dohledávání běží hodiny, a když se proces zabije uprostřed, začínalo by se od nuly.

Řešení: MappingGenerator pracuje checkpoint-based — po každé batch dávce ukládá stav do mapping_generation_checkpoint.json. Kill process, spusť znovu, pokračuje tam, kde skončil. Zároveň se ukládají mezivýsledky do CSV pro backup.

Snapshot z produkčního stavu k dnešnímu dni — z databáze a repozitáře, žádné odhady.

8 měsíců produkčního provozu
11 888 mapování v databázi
66 color mappingů (CZ ↔ BL)
~14 000 řádků Python kódu
17 HTML šablon admin panelu
3 min polling interval BrickLink
30 min orphan auto-resume window
2 externí API integrace

[Doplnit business metriky od majitele — objem objednávek za den, čas ušetřený na manuální synchronizaci, případně počet předešlých oversellingů. Bez těchto dat je stránka technicky popsaná, ale postrádá impact story.]