// /admin.html → WEBSITE tab. Per-product operational state editor backed by // /api/admin/product-states. Writes propagate to every visitor immediately // because the catalog/spotlight/modal/status page all read from the same // /api/site/product-states feed via window.ExecStates. (function () { const { useState, useEffect, useMemo, useCallback } = React; const STATE_OPTIONS = [ { id: 'online', label: 'ONLINE' }, { id: 'updating', label: 'UPDATING' }, { id: 'offline', label: 'OFFLINE' }, ]; function AdminSiteApp() { // Re-render whenever ExecStates emits. window.ExecStates && window.ExecStates.useStates(); window.ExecCatalog && window.ExecCatalog.useCatalog(); window.ExecCategories && window.ExecCategories.useCategories(); const products = window.ExecCatalog ? window.ExecCatalog.list() : []; const counts = window.ExecStates ? window.ExecStates.counts(products) : { online: 0, offline: 0, updating: 0, total: products.length }; const [category, setCategory] = useState('all'); const categories = useMemo(() => { const counts = {}; for (const p of products) counts[p.category] = (counts[p.category] || 0) + 1; const live = window.ExecCategories ? window.ExecCategories.list() : []; return [ { id: 'all', label: 'ALL', count: products.length }, ...live.map((c) => ({ ...c, count: counts[c.id] || 0 })), ]; }, [products]); const filtered = category === 'all' ? products : products.filter((p) => p.category === category); return ( <> 0}/> 0}/>
// WEBSITE

PRODUCT STATES / {filtered.length}

CATEGORY
Changes save instantly. The catalog, status page, and every counter on the site reflect updates within a second.
{filtered.map((p) => )}
); } function StateRow({ product }) { const current = window.ExecStates ? window.ExecStates.get(product.id) : { state: 'updating', note: '' }; const [note, setNote] = useState(current.note || ''); const [saving, setSaving] = useState(null); // null | 'state' | 'note' const [error, setError] = useState(null); const [lastSaved, setLastSaved] = useState(null); // Sync local note when remote changes from outside (e.g. another tab). useEffect(() => { setNote(current.note || ''); }, [current.note]); const save = useCallback(async (nextState, nextNote, kind) => { setSaving(kind); setError(null); try { await window.ExecStates.setState(product.id, nextState, nextNote); setLastSaved(Date.now()); } catch (e) { setError(String(e && e.message || e)); } finally { setSaving(null); } }, [product.id]); const onPickState = (e) => save(e.target.value, note, 'state'); const onBlurNote = () => { if ((note || '') === (current.note || '')) return; save(current.state, note, 'note'); }; return (
{product.tag}
{(product.category || '').toUpperCase()} {product.name}
{current.state.toUpperCase()}
setNote(e.target.value)} onBlur={onBlurNote} onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur(); }} maxLength={240} disabled={saving === 'note'} />
{saving && saving…} {!saving && error && save failed} {!saving && !error && lastSaved && saved}
); } window.AdminSiteApp = AdminSiteApp; })();