/* Realtime inventory (24 shelves) + Shelves tab + Shelf drawer */
const STATE_LABELS = (lang) => ({
full: I18N[lang].state_full,
empty: I18N[lang].state_empty,
vacant: I18N[lang].state_vacant,
disabled: I18N[lang].state_disabled,
error: I18N[lang].state_error,
});
const Inventory = ({ machine, lang }) => {
const t = I18N[lang];
const [selected, setSelected] = React.useState(null);
const sl = STATE_LABELS(lang);
// Mobil: detaljpanelet ligger UNDER hylle-flisene når kolonnene stables —
// scroll det inn når en hylle velges, ellers ser det ut som ingenting skjer.
const detailRef = React.useRef(null);
const velgHylle = React.useCallback((sh) => {
setSelected(sh);
if (sh && window.matchMedia('(max-width: 900px)').matches) {
setTimeout(() => detailRef.current?.scrollIntoView(
{ behavior: 'smooth', block: 'start' }), 60);
}
}, []);
// EKTE sanntid (2026-09-03): poll hyllene hvert 10. sek. Før var visningen
// et statisk øyeblikksbilde fra innlogging — innkomne returflasker
// (empty_bottle) fra kundebytter dukket aldri opp.
const [rev, setRev] = React.useState(0);
const [lastSync, setLastSync] = React.useState(Date.now());
const syncShelves = React.useCallback(async () => {
try {
const fresh = await window.api.refreshMachineShelves(machine.id);
machine.shelves.splice(0, machine.shelves.length, ...fresh);
setSelected(sel => sel ? (fresh.find(f => f.rawShelfId === sel.rawShelfId) || null) : null);
setLastSync(Date.now());
setRev(r => r + 1);
} catch (e) { /* nettverksglipp — prøver igjen neste tick */ }
}, [machine]);
React.useEffect(() => {
const iv = setInterval(syncShelves, 10000);
return () => clearInterval(iv);
}, [syncShelves]);
// Dynamic counts: state-counters + per-bottle-code counters
// (vi vet ikke navnene på flasketypene før vi har sett dataen, så vi
// bygger en map per kode → {name, image, full, empty})
const { counts, byType } = React.useMemo(() => {
const c = { full: 0, empty: 0, vacant: 0, disabled: 0, error: 0 };
const t = {}; // code → { name, image, full, empty }
machine.shelves.forEach(s => {
c[s.state] = (c[s.state] || 0) + 1;
if (s.bottle) {
if (!t[s.bottle]) t[s.bottle] = { name: s.bottleName || s.bottle, image: s.bottleImage, full: 0, empty: 0 };
if (s.state === 'full') t[s.bottle].full++;
if (s.state === 'empty') t[s.bottle].empty++;
}
});
return { counts: c, byType: t };
}, [machine.shelves, rev]);
const bottleEntries = Object.entries(byType).sort((a, b) => (b[1].full + b[1].empty) - (a[1].full + a[1].empty));
return (
<>
{t.tab_inventory}
{/* Totaler for HELE automaten (ikke per seksjon) — samme
typografi for full/tom/ledig. */}
{counts.full} full
{counts.empty} tom
{counts.vacant} ledig
{t.live}
{lang === 'no' ? 'Oppdatert' : 'Updated'} {Math.max(0, Math.round((Date.now() - lastSync) / 1000))}s {lang === 'no' ? 'siden' : 'ago'}
{t.refresh}
{sl.full} ({counts.full})
{sl.empty} ({counts.empty})
{sl.vacant} ({counts.vacant})
{sl.disabled} ({counts.disabled})
{sl.error} ({counts.error})
{lang === 'no' ? 'Beholdning' : 'Inventory'}
{bottleEntries.length === 0 && (
{lang === 'no' ? 'Ingen flasker registrert' : 'No bottles registered'}
)}
{bottleEntries.map(([code, info]) => (
0 ? `${info.empty} ${sl.empty.toLowerCase()}` : null}
val={info.full} />
))}
{selected ? (
setSelected(null)}
onReload={machine._onReload}
onRefresh={syncShelves} />
) : (
{lang === 'no' ? 'Velg en hylle' : 'Select a shelf'}
{lang === 'no' ? 'Klikk en hylle for detaljer, kamerabilde og styring' : 'Click a shelf for details, camera image and control'}
)}
>
);
};
const InvRow = ({ color, label, val, bordered, image, sub }) => (
{image ? (
{ e.target.style.display = 'none'; }} />
) : (
)}
{label}
{sub && {sub} }
{val}
);
const Cabinets = ({ machine, selected, onSelect, lang }) => {
const t = I18N[lang];
const A = machine.shelves.filter(s => s.cabinet === 'A');
const B = machine.shelves.filter(s => s.cabinet === 'B');
return (
);
};
const Cabinet = ({ title, sub, shelves, selected, onSelect, lang }) => {
const sl = STATE_LABELS(lang);
// Totaler vises samlet i panel-hodet (2026-09-04) — ikke per seksjon.
return (
);
};
/* En enkelt hylle — kompakt flis med tydelig flasketype i ramma */
const ShelfTile = ({ sh, selected, onSelect, sl }) => {
const bottleImg = sh.bottleImage
? `/app/img/bottles/${sh.bottleImage}`
: null;
const isFull = sh.state === 'full';
const isEmpty = sh.state === 'empty';
const isVacant = sh.state === 'vacant';
const isError = sh.state === 'error';
const isDisabled = sh.state === 'disabled';
const stateClass = `state-${sh.state}`;
// Kort, tydelig typeetikett: «11 kg» + materiale (Stål/Kompositt/…)
const wt = sh.bottle ? window.bottleHelpers?.weightKg(sh.bottle) : null;
const mat = sh.bottle ? window.bottleHelpers?.material(sh.bottle) : null;
const matLabel = mat ? (window.bottleHelpers?.materialLabels?.[mat]?.single || '') : '';
return (
onSelect(sh)}>
{/* topp-stripe: dør-id og status-badge */}
{sh.id}
{sl[sh.state]}
{/* hoved-innhold: lite flaskebilde + STOR typeetikett */}
{bottleImg && (isFull || isEmpty) && (
{ e.target.style.display = 'none'; }} />
)}
{(isFull || isEmpty) && sh.bottle && (
{wt ? `${wt} kg` : (sh.bottleName || sh.bottle)}
{matLabel && {matLabel} }
)}
{isVacant && (
ledig
)}
{isDisabled && (
deaktivert
)}
{isError && (
feil
)}
{/* hjørne-indikator */}
{isFull &&
●
}
{isEmpty &&
○
}
);
};
const ShelfDetail = ({ shelf, machine, lang, onClose, onReload, onRefresh }) => {
const t = I18N[lang];
const sl = STATE_LABELS(lang);
const isSuper = !!(window.isSuperView && window.isSuperView());
const [busy, setBusy] = React.useState(null);
const safe = async (label, fn) => {
setBusy(label);
try {
await fn();
// onRefresh (hylle-poll) oppdaterer den VALGTE hylla straks — bl.a. det
// nye bildet etter «Ta nytt bilde» (URL-en er versjonert på bilde-id).
if (onRefresh) await onRefresh();
if (onReload) await onReload();
}
catch (e) { alert(`${label}: ${e.message}`); }
finally { setBusy(null); }
};
return (
{lang === 'no' ? 'Hylle' : 'Shelf'} {shelf.id} · {sl[shelf.state]}
{/* Flaska i kammeret — samme illustrasjon som salgsskjermen. For
test-automater ER dette den simulerte flaska. */}
{shelf.bottle && (
{ e.target.onerror = null; e.target.src = '/app/img/bottles/generic.svg'; }} />
{shelf.bottleName || shelf.bottle}
{sl[shelf.state]}{machine.isTest ? ' · simulert' : ''}
)}
{/* AI-konfidens er plattform-/driftsinfo — kun superadmin */}
{isSuper && (
)}
{t.shelf_actions}
{machine.kind === 'legacy' ? (
) : (
safe('Åpne dør', () => window.api.testDoor(machine.id, shelf.rawDoor, true))}>
{t.btn_unlock}
safe('Ta nytt bilde', async () => {
// Ikke-invasiv hyllesjekk: kameraet som SER hylla fotograferer,
// ML kjører — poll til resultatet er klart (som /prod).
await window.api.recheckShelf(machine.id, shelf.rawShelfId);
for (let i = 0; i < 20; i++) {
await new Promise(r => setTimeout(r, 1500));
const r = await window.api.checkResult(machine.id, shelf.rawShelfId);
if (!r.pending) return;
}
throw new Error('Ingen svar fra kameraet på 30 s — er modulen online?');
})}>
{t.btn_capture}
{shelf.bottle && shelf.state === 'full' && (
safe('Marker tomflaske', () => window.api.overrideShelf(machine.id, shelf.rawShelfId, shelf.bottle, 'empty_bottle'))}>
{lang === 'no' ? 'Marker tomflaske' : 'Mark empty bottle'}
)}
safe('Marker ledig', () => window.api.overrideShelf(machine.id, shelf.rawShelfId, null, 'empty'))}>
{lang === 'no' ? 'Marker ledig' : 'Mark vacant'}
{shelf.state !== 'error' ? (
safe('Deaktiver', () => window.api.overrideShelf(machine.id, shelf.rawShelfId, null, 'faulty'))}>
{t.btn_disable}
) : (
safe('Aktiver', () => window.api.overrideShelf(machine.id, shelf.rawShelfId, null, 'empty'))}>
{t.btn_enable}
)}
)}
{busy &&
⏳ {busy}…
}
);
};
/* === LEGACY HANDLINGER === */
const LegacyShelfActions = ({ shelf, machine, onReload, busy, safe }) => {
const [shadowResult, setShadowResult] = React.useState(null);
const [running, setRunning] = React.useState(null);
// Skygge-sjekk (v1 vs v2-ML m/ konfidenser) er plattform-diagnostikk —
// kun superadmin-visning
const isSuper = !!(window.isSuperView && window.isSuperView());
const doShadow = async (useExisting = false) => {
setShadowResult(null);
setRunning(useExisting ? 'shadow-existing' : 'shadow');
try {
const door = shelf.door || shelf.rawDoor;
const r = await window.api.legacyShadowCheck(machine.id, door, 30, useExisting);
setShadowResult(r);
if (!useExisting && onReload) await onReload();
} catch (e) { alert('Skygge-sjekk: ' + (e?.message || JSON.stringify(e))); }
finally { setRunning(null); }
};
const doCheck = () => safe('Ta nytt bilde (v1)',
() => window.api.legacyCheckDoor(machine.id, shelf.door));
const doOpen = () => {
if (!confirm(
`⚠ ÅPNER FAKTISK DØR #${shelf.door} på ${machine.location}!\n\n` +
`Dette KAN forstyrre en pågående kunde-handel. Sikker?`
)) return;
if (!confirm('Bekreft enda en gang — døren åpnes umiddelbart.')) return;
safe('Dør åpnet', () => window.api.legacyOpenDoor(machine.id, shelf.door));
};
return (
<>
ℹ v1 (gammel) automat — i drift. Kommandoer skrives til Firestore
og kan ta 2-15 sekunder før v1 RPi plukker dem opp.
Ta nytt bilde (v1-ML)
{isSuper && (<>
doShadow(true)}>
v2-sjekk eksisterende bilde
doShadow(false)}>
Skygge-sjekk (nytt bilde + v1+v2)
>)}
Åpne dør
{running === 'shadow-existing' && (
⏳ Kjører v2-ML mot eksisterende bilde…
)}
{running === 'shadow' && (
⏳ Venter på at v1 tar bilde og laster opp… (opptil 30 sek)
)}
{shadowResult && isSuper && }
>
);
};
const ShadowResult = ({ result }) => {
if (!result.ok) {
return (
❌ Skygge-sjekk feilet:
{result.reason}
{result.note &&
{result.note}
}
);
}
const { v1, v2, agreement } = result;
return (
Skygge-sjekk resultat
{result.used_existing && EKSISTERENDE BILDE }
{agreement.valve_match && agreement.bottle_match
? v1 og v2 ENIG ✓
: UENIGHET ⚠ }
{result.ml_stub && (
⚠ v2-ML kjører i stub-modus — ingen TensorFlow-modell lastet,
så v2-resultatet er tilfeldig generert. Last opp modell til /app/models/bottle_v3
og /app/models/valve_v1 for ekte ML.
)}
v1 (live)
{v1.label || '—'}
Ventil: {v1.valve || '—'} · konfidens: {v1.score != null ? (v1.score * 100).toFixed(0) + '%' : '—'}
v2 (skygge)
{v2.bottle?.label || '—'}
Ventil: {v2.valve?.label || '—'} ({((v2.valve?.score || 0) * 100).toFixed(0)}%) ·
Flaske: {((v2.bottle?.score || 0) * 100).toFixed(0)}%
Ventil-match: {agreement.valve_match ? '✓' : '✗'}
Flaske-match: {agreement.bottle_match ? '✓' : '✗'}
v2 kombinert OK: {agreement.v2_combined_ok ? '✓' : '✗'}
{v2.mismatch && ⚠ v2 valve/bottle mismatch }
{/* === Detaljer: top-3 alternativer + anti-spoof + presence === */}
{v2.bottle?.top3 && v2.bottle.top3.length > 0 && (
v2 top-3 alternativer (embedding cosine-sim)
{v2.bottle.top3.map((t, i) => (
{t.code}
{(t.score * 100).toFixed(1)}%
))}
{v2.bottle?.margin != null && (
Margin til 2.-plass: {(v2.bottle.margin * 100).toFixed(1)} pp
{v2.bottle.margin < 0.05 && ⚠ liten margin, usikker }
)}
)}
{v2.anti_spoof && (
Anti-spoof analyse
{v2.anti_spoof.is_spoof ? '⚠ MISTENKELIG' : '✓ EKTE'} ({(v2.anti_spoof.spoof_score * 100).toFixed(0)}%)
{Object.entries(v2.anti_spoof.signals || {}).map(([k, v]) => (
{k}
0.55 ? 'var(--warn)' : 'inherit' }}>
{(v * 100).toFixed(0)}%
))}
{v2.anti_spoof.reasons?.length > 0 && (
⚠ {v2.anti_spoof.reasons.join(' · ')}
)}
)}
{v2.presence && (
Objekt-presence: {v2.presence.has_bottle ? '✓' : '—'}
({(v2.presence.confidence * 100).toFixed(0)}% bottle-lignende)
)}
{v2.pipeline_version && (
Pipeline: {v2.pipeline_version}
{v2.using_embedding_classifier && ' · embedding cosine-sim'}
)}
);
};
/* Force-set bottle type dropdown.
2026-09-04: (a) lista rendres position:fixed ut fra knappen så den ALDRI
klippes av panel-ramma (før ble den skjult bak hylle-info); (b) viser kun
flasketyper som er i bruk på automaten (sortimentet + det som står i
hyllene) — ikke hele plattform-katalogen. */
const BottleOverride = ({ shelf, machine, lang, onDone }) => {
const [open, setOpen] = React.useState(false);
const [pos, setPos] = React.useState(null);
const [inUse, setInUse] = React.useState(null); // Set av bottle-koder
const btnRef = React.useRef(null);
React.useEffect(() => {
let dau = false;
// Koder som står i automaten akkurat nå (alltid relevante)
const shelfCodes = new Set((machine.shelves || [])
.map(s => s.bottle).filter(Boolean));
if (!machine.tenantId) { setInUse(shelfCodes); return; }
window.api.listPriceOverrides(machine.tenantId, machine.id)
.then(rows => {
if (dau) return;
rows.filter(r => r.offered).forEach(r => shelfCodes.add(r.code));
setInUse(new Set(shelfCodes));
})
.catch(() => { if (!dau) setInUse(shelfCodes); });
return () => { dau = true; };
}, [machine.id]);
const bottles = (window.DATA?.BOTTLE_TYPES || [])
.filter(b => !inUse || inUse.size === 0 || inUse.has(b.id));
const toggle = () => {
if (!open && btnRef.current) {
const r = btnRef.current.getBoundingClientRect();
// Åpne opp/ned avhengig av plass; fixed = klippes aldri av paneler.
// maxHeight klemmes mot FAKTISK plass (lav viewport / telefon i landskap).
const plassNed = window.innerHeight - r.bottom - 12;
const plassOpp = r.top - 12;
const nedover = plassNed >= Math.min(300, plassOpp) || plassNed >= 200;
setPos({
left: Math.max(8, Math.min(r.left, window.innerWidth - 260)),
top: nedover ? r.bottom + 4 : null,
bottom: nedover ? null : (window.innerHeight - r.top + 4),
maxH: Math.max(120, Math.min(300, nedover ? plassNed : plassOpp)),
});
}
setOpen(o => !o);
};
return (
<>
{lang === 'no' ? 'Sett type ▾' : 'Set type ▾'}
{open && pos && (
<>
setOpen(false)} />
{bottles.length === 0 && (
{lang === 'no' ? 'Ingen flasketyper i sortimentet' : 'No bottle types in assortment'}
)}
{bottles.map(b => (
{
setOpen(false);
try {
await window.api.overrideShelf(machine.id, shelf.rawShelfId, b.id, 'full');
if (onDone) await onDone();
} catch (e) { alert(e.message); }
}}>
{b.name}
))}
>
)}
>
);
};
const KVP = ({ label, value, colspan = 1 }) => (
);
/* Camera shot — EKTE bilde fra backend. Ingen tegnet SVG-mock lenger
(mock-revisjon 2026-09-04): finnes ikke bilde, sier vi det ærlig. */
const CamShot = ({ machineId, shelfId, cameraId, bottle, state, lang, realImageUrl }) => {
const [imgSrc, setImgSrc] = React.useState(null);
const [feil, setFeil] = React.useState(null);
React.useEffect(() => {
if (!realImageUrl) { setImgSrc(null); setFeil(null); return; }
let cancelled = false;
setFeil(null);
window.authedImageUrl(realImageUrl).then(url => { if (!cancelled) setImgSrc(url); })
.catch(e => { if (!cancelled) { setImgSrc(null); setFeil(e.message); } });
return () => { cancelled = true; };
}, [realImageUrl]);
if (imgSrc) {
return (
CAM-{cameraId} · {shelfId}
{lang === 'no' ? 'Sist tatt' : 'Last taken'}
);
}
return (
{feil
? (lang === 'no' ? `Kunne ikke hente bildet (${feil})` : `Could not load image (${feil})`)
: (lang === 'no' ? 'Ingen bilde tatt av denne hyllen ennå — bruk «Ta nytt bilde»'
: 'No image captured for this shelf yet')}
CAM-{cameraId} · {shelfId}
);
};
const BottleSvg = ({ type, x, y, scale = 1, faded = false }) => {
// Farge + størrelse fra material + vekt (utledet fra bottle.code)
const mat = window.bottleHelpers?.material(type) || 'unknown';
const weight = window.bottleHelpers?.weightKg(type);
const color = ({
stal: '#94a3b8',
lettstal: '#cbd5e1',
kompositt: '#fb923c',
aluminium: '#60a5fa',
})[mat] || '#f59e0b';
// Liten flaske = lite høyde, stor = mye
const w = weight && weight <= 5 ? 38 : (weight && weight >= 11 ? 48 : 44);
const h = weight && weight <= 5 ? 68 : (weight && weight >= 11 ? 95 : 82);
const shortLabel = weight ? `${weight}kg` : '?';
return (
{shortLabel}
);
};
/* Liten EKTE hyllebilde-thumbnail (lazy, nedskalert server-side) */
const ShelfThumb = ({ shelf }) => {
const [src, setSrc] = React.useState(null);
const [visible, setVisible] = React.useState(false);
const ref = React.useRef(null);
const url = shelf.lastImageUrl
? shelf.lastImageUrl + (shelf.lastImageUrl.includes('?') ? '&' : '?') + 'size=96'
: null;
React.useEffect(() => {
if (!ref.current || !url) return;
const obs = new IntersectionObserver(entries => {
entries.forEach(e => { if (e.isIntersecting) { setVisible(true); obs.disconnect(); } });
}, { rootMargin: '200px' });
obs.observe(ref.current);
return () => obs.disconnect();
}, [url]);
React.useEffect(() => {
if (!visible || !url) return;
let cancelled = false;
window.authedImageUrl(url)
.then(u => { if (!cancelled) setSrc(u); })
.catch(() => { if (!cancelled) setSrc(null); });
return () => { cancelled = true; };
}, [url, visible]);
return (
{src ? (
) : url ? null : (
)}
);
};
/* ===== Shelves tab — table with last image thumb and per-shelf actions ===== */
const ShelvesTab = ({ machine, lang }) => {
const t = I18N[lang];
const sl = STATE_LABELS(lang);
const isSuper = !!(window.isSuperView && window.isSuperView());
const [filter, setFilter] = React.useState('all');
const [open, setOpen] = React.useState(null);
const filtered = machine.shelves.filter(s => filter === 'all' || s.state === filter);
// CSV-eksport av hyllelista, klientside (2026-09-05 — knappen hadde ingen
// handler før). Kolonner: hylle, dør, kamera, status, flaske, sist bilde.
const exportCsv = () => {
const cell = v => `"${String(v ?? '').replace(/"/g, '""')}"`;
const lines = [
['Hylle', 'Dør', 'Kamera', 'Status', 'Flaske', 'Sist bilde'].join(';'),
...filtered.map(sh => [
sh.id, sh.rawDoor, `CAM-${sh.cameraId}`, sl[sh.state] || sh.state,
sh.bottleName || sh.bottle || '', sh.lastEventAt !== '—' ? sh.lastEventAt : '',
].map(cell).join(';')),
].join('\n');
// BOM så Excel leser UTF-8 (æøå) riktig
const blob = new Blob(['\ufeff' + lines], { type: 'text/csv;charset=utf-8' });
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `hyller-${machine.id}.csv`;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
};
return (
<>
{lang === 'no' ? 'Alle 24 hyller' : 'All 24 shelves'}
{filtered.length}/{machine.shelves.length}
setFilter('all')}>{lang === 'no' ? 'Alle' : 'All'}
setFilter('full')}>{sl.full}
setFilter('empty')}>{sl.empty}
setFilter('vacant')}>{sl.vacant}
setFilter('error')}>{sl.error}
CSV
{lang === 'no' ? 'Hylle' : 'Shelf'}
{lang === 'no' ? 'Status' : 'Status'}
{lang === 'no' ? 'Innhold' : 'Contents'}
{lang === 'no' ? 'Kamera' : 'Camera'}
{/* «Lås»-kolonnen er fjernet (2026-09-05): lockState var en
konstant 'locked' uten kilde i backend. */}
{t.last_event}
{filtered.map(sh => (
setOpen(sh)} style={{ cursor: 'pointer' }}>
{/* EKTE hyllebilde-thumbnail (2026-09-04) — før: svart
boks med tegnet SVG-flaske. */}
{sh.id}
{sh.cabinet} · R{sh.row + 1}/K{sh.col + 1}
{sl[sh.state]}
{sh.bottleName ? {sh.bottleName} : — }
CAM-{sh.cameraId}
{sh.lastEvent} · {sh.lastEventAt}
e.stopPropagation()}>
{/* Ekte handlinger (mock-revisjon 2026-09-04) — samme
endepunkter som Lagerstatus-fanen bruker. */}
window.api.testDoor(machine.id, sh.rawDoor, true)
.catch(e => alert(e.message))}>
{t.btn_unlock}
window.api.recheckShelf(machine.id, sh.rawShelfId)
.then(() => alert(lang === 'no' ? 'Bilde bestilt — se Lagerstatus om noen sekunder' : 'Capture requested'))
.catch(e => alert(e.message))}>
))}
{open && (
setOpen(null)} title={`${lang === 'no' ? 'Hylle' : 'Shelf'} ${open.id}`}>
{/* Ekte hyllebilde (mock-revisjon 2026-09-04): siste ROI-utsnitt fra
backend i stedet for tegnet SVG med falsk REC-HUD. */}
{/* AI-konfidens er plattforminfo — kun superadmin */}
{isSuper && (
)}
{t.shelf_actions}
window.api.testDoor(machine.id, open.rawDoor, true)
.catch(e => alert(e.message))}>
{t.btn_unlock}
window.api.recheckShelf(machine.id, open.rawShelfId)
.then(() => alert(lang === 'no' ? 'Bilde bestilt' : 'Capture requested'))
.catch(e => alert(e.message))}>
{t.btn_capture}
{
if (!confirm(lang === 'no' ? 'Deaktivere hyllen (faulty)?' : 'Disable shelf?')) return;
window.api.overrideShelf(machine.id, open.rawShelfId, null, 'faulty')
.then(() => { alert('OK'); setOpen(null); })
.catch(e => alert(e.message));
}}>{t.btn_disable}
)}
>
);
};
const Drawer = ({ title, onClose, children }) => (
<>
>
);
Object.assign(window, { Inventory, ShelvesTab, BottleSvg, CamShot, Drawer });