/* ═══════════════════════════════════════════════════════════ Main app — icons, login, overview dashboard, app shell (rail nav + topbar + theme toggle), Root state + mount. This is the reconstructed entry that ties every view + modal together. ═══════════════════════════════════════════════════════════ */ (function () { const { useState: useStateA, useEffect: useEffectA, useReducer, useRef: useRefA } = React; const { GUNS: GUNS_A, parseKey: parseKeyA, keyLabel: keyLabelA, fmtDatetime: fmtA, totalAmmo: totalAmmoA, totalAmmoGuns: totalAmmoGunsA, totalAmmoBunker: totalAmmoBunkerA, calcGroupTotal: calcGroupTotalA, buildInit: buildInitA, applyMigrations: applyMigrationsA, reducer: reducerA, saveState: saveStateA, } = window; /* ── Icons (inline stroke SVG) ──────────────────────────── */ const ICON_PATHS = { grid: 'M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z', layers: 'M12 2 2 7l10 5 10-5zM2 17l10 5 10-5M2 12l10 5 10-5', box: 'M21 8 12 3 3 8v8l9 5 9-5zM3 8l9 5 9-5M12 13v8', target: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM12 6a6 6 0 1 0 0 12 6 6 0 0 0 0-12zM12 10a2 2 0 1 0 0 4 2 2 0 0 0 0-4z', swap: 'M16 3l4 4-4 4M20 7H8M8 21l-4-4 4-4M4 17h12', ban: 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM5 5l14 14', barrel: 'M5 4h14M5 4v16M19 4v16M5 20h14M8 4v16M16 4v16', list: 'M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01', settings: 'M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-2.82 1.17V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9c.2.61.76 1 1.4 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z', sun: 'M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4', moon: 'M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z', logout: 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9', menu: 'M3 12h18M3 6h18M3 18h18', plus: 'M12 5v14M5 12h14', cloud: 'M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z', bolt: 'M13 2 3 14h9l-1 8 10-12h-9z', shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z', }; function Icon({ name, size = 18, stroke = 2, fill = false }) { const d = ICON_PATHS[name] || ''; return ( ); } /* ── Toast ──────────────────────────────────────────────── */ function useToast() { const [toast, setToast] = useStateA(null); const show = (msg, kind = 'ok') => { setToast({ msg, kind }); setTimeout(() => setToast(null), 2400); }; const node = toast ?
{toast.msg}
: null; return [node, show]; } /* ── Login screen ───────────────────────────────────────── */ const ROLE_META = { viewer: { label: 'צפייה בלבד', color: 'var(--text-3)', icon: '👁' }, operator: { label: 'מפעיל', color: 'var(--ok)', icon: '⚡' }, admin: { label: 'מנהל', color: 'var(--accent)', icon: '🔐' }, }; function LoginScreen({ onAuthed, sessionExpired }) { const cfg = window.APP_CONFIG || {}; const [code, setCode] = useStateA(''); const [err, setErr] = useStateA(''); const [busy, setBusy] = useStateA(false); const [role, setRole] = useStateA(null); // show after login const submit = async () => { if (!code || busy) return; setBusy(true); setErr(''); try { const d = await window.api.login(code); setRole(d.role); // Brief role flash then continue setTimeout(() => onAuthed(), 900); } catch { setErr('קוד שגוי — נסה שוב'); setCode(''); } finally { setBusy(false); } }; if (role) { const m = ROLE_META[role] || ROLE_META.viewer; return (
{m.icon}
{m.label}
כניסה מאושרת...
); } return (

{cfg.appName || 'ניהול תחמושת סוללתית'}

{cfg.appShort || 'ARTY · AMMO'} · גרסה {cfg.version || ''}

{sessionExpired && (
⏱ הסשן פג — יש להתחבר מחדש
)} {err &&
{err}
} setCode(e.target.value)} onKeyDown={e => e.key === 'Enter' && submit()} style={{ textAlign: 'center', fontSize: '1.05rem', letterSpacing: '.12em' }} /> {cfg.roles?.hasOperator && (
👁 צפייה · ⚡ מפעיל · 🔐 מנהל
)}

אימות HMAC מאובטח בצד השרת

); } /* ── Overview dashboard ─────────────────────────────────── */ function StatCard({ icon, label, value, meta, accent }) { return (
{icon}{label} {value} {meta && {meta}}
); } function categoryTotals(state) { const gun = totalAmmoGunsA(state); const ramsav = totalAmmoBunkerA(state); const outGun = { shell: 0, charge: 0, fuze: 0 }; const outRamsav = { shell: 0, charge: 0, fuze: 0 }; (state.registeredTypes || []).forEach(r => { outGun[r.category] = (outGun[r.category] || 0) + (gun[r.key] || 0); outRamsav[r.category] = (outRamsav[r.category] || 0) + (ramsav[r.key] || 0); }); return { gun: outGun, ramsav: outRamsav }; } /* ── Custom domain icons (inline SVG) ───────────────────── */ function ShellIcon({ size = 16 }) { // Artillery shell silhouette return ( ); } function MaintenanceIcon({ size = 16 }) { // Wrench — cleaning/maintenance return ( ); } function BrokenBarrelIcon({ size = 16 }) { // Cracked barrel — barrel wear return ( ); } function readinessOf(state, g) { if (state.readiness?.red === g) return { cls: 'dot-danger', label: 'כוננות צוות אדום' }; if (state.readiness?.yellow === g) return { cls: 'dot-warn', label: 'כוננות צוות צהוב' }; return { cls: 'dot-ok', label: 'כוננות צוות ירוק' }; } function GunMiniCard({ state, gun }) { const regs = state.registeredTypes; const inv = state.guns[gun] || {}; const items = regs.filter(r => (inv[r.key] || 0) > 0).sort((a, b) => (inv[b.key] || 0) - (inv[a.key] || 0)); const top = items.slice(0, 4); const r = readinessOf(state, gun); const bd = state.barrelData?.[gun] || {}; const totalShells = bd.totalShells || 0; return (
{gun} {r.label}
{top.length === 0 ? אין מלאי : top.map(it => (
{it.label}{inv[it.key]}
))} {items.length > 4 && +{items.length - 4} סוגים נוספים}
{bd.shellsSinceClean || 0} נקיון
{bd.muzzle?.count || 0} קנה
{totalShells} סה"כ
); } function ActivityFeed({ state }) { const regs = state.registeredTypes; const fires = (state.firingLog || []).slice(0, 6).map(r => ({ kind: 'fire', ts: (r.date || '') + ' ' + (r.time || ''), title: `משימת אש ${r.missionId} — ${r.target || ''}`, sub: `צוות ${r.gun} · ${r.unit || ''}`, })); const moves = [...(state.transferLog || [])].slice(-6).reverse().map(r => ({ kind: 'move', ts: fmtA(r.datetime), title: `${r.src} ← ${r.dst}`, sub: (r.items || []).map(it => `${keyLabelA(it.key, regs)}×${it.qty}`).join(' · '), })); const feed = [...fires, ...moves].slice(0, 8); const meta = { fire: { icon: , color: 'var(--danger)', soft: 'var(--danger-soft)' }, move: { icon: , color: 'var(--info)', soft: 'var(--info-soft)' }, }; return (
פעילות אחרונה
{feed.length === 0 ?
אין פעילות עדיין
: (
{feed.map((f, i) => (
{meta[f.kind].icon}
{f.title}{f.sub} · {f.ts}
))}
)}
); } function OverviewView({ state }) { const cats = categoryTotals(state); const redTeam = state.readiness?.red, yellowTeam = state.readiness?.yellow; const totalsGun = totalAmmoGunsA(state); const critical = (state.registeredTypes || []).filter(r => { const rl = state.redLines?.[r.key] || 0; return rl > 0 && (totalsGun[r.key] || 0) < rl; }); const groups = (state.ammoGroups || []).map(g => ({ ...g, ...calcGroupTotalA(g, state) })); const critGroups = groups.filter(g => g.redLine > 0 && g.total < g.redLine); return (
{(redTeam || yellowTeam || critical.length > 0 || critGroups.length > 0) && (
{redTeam &&
צוות {redTeam} כוננות צוות אדום
} {yellowTeam &&
צוות {yellowTeam} כוננות צוות צהוב
} {critical.length > 0 &&
⚠ {critical.length} סוגי תחמושת מתחת לקו האדום
} {critGroups.map(g =>
⚠ קבוצה {g.label}: {g.total} / קו {g.redLine}
)}
)}
} label="פגזים קרקע" value={cats.gun.shell} meta="ערום צוותים" accent /> } label="פגזים רמסע" value={cats.ramsav.shell} meta="משטחים/מחסן" /> } label='חנ"ה קרקע' value={cats.gun.charge} /> } label="מרעומים קרקע" value={cats.gun.fuze} /> } label="משימות אש" value={(state.firingLog || []).length} /> } label="פינת פסולים" value={(state.quarantineLog || []).length} />
{/* Ammo groups summary — only shown when groups exist */} {groups.length > 0 && (
📦 קבוצות תחמושת
{groups.map(g => { const crit = g.redLine > 0 && g.total < g.redLine; const pct = g.redLine > 0 ? Math.min(100, Math.round(g.total / g.redLine * 100)) : null; return (
{g.label} {crit && ⚠ מתחת לקו}
{g.total} סה"כ
{g.gunTotal} קרקע
{g.bunkerTotal} רמסע
{g.redLine > 0 && (
{g.redLine} קו אדום
)}
{pct !== null && (
)}
); })}
)} {/* Bottom section fills remaining height on PC */}
מצב צוותים{GUNS_A.length} צוותים
{GUNS_A.map(g => )}
); } /* ── App shell ──────────────────────────────────────────── */ const NAV = [ { sec: 'ראשי', items: [{ id: 'overview', label: 'סקירה כללית', icon: 'grid' }] }, { sec: 'מלאי', items: [ { id: 'teams', label: 'מטריצת צוותים', icon: 'layers' }, { id: 'bunker', label: 'מחסן ומשטחים', icon: 'box' }, { id: 'barrel', label: 'בלאי קנה', icon: 'barrel' }, ] }, { sec: 'תיעוד', items: [ { id: 'missions', label: 'יומן משימות אש', icon: 'target' }, { id: 'transfers', label: 'יומן תנועות', icon: 'swap' }, { id: 'quarantine', label: 'פינת פסולים', icon: 'ban' }, ] }, { sec: 'מערכת', items: [{ id: 'catalog', label: 'קטלוג תחמושת', icon: 'settings' }] }, ]; const VIEW_TITLES = { overview: 'סקירה כללית', teams: 'מטריצת מלאי צוותים', bunker: 'מחסן ומשטחים', barrel: 'בלאי קנה', missions: 'יומן משימות אש', transfers: 'יומן תנועות מלאי', quarantine: 'פינת פסולים', catalog: 'קטלוג תחמושת מרכזי', }; function App({ state, dispatch, loadingCloud, theme, onToggleTheme, onLogout }) { const [view, setView] = useStateA('overview'); const [drawer, setDrawer] = useStateA(false); const [modal, setModal] = useStateA(null); const [adminModal, setAdminModal] = useStateA(null); const [resetConfirm, setResetConfirm] = useStateA(false); const [importFile, setImportFile] = useStateA(null); const [isOnline, setIsOnline] = useStateA(() => window.api.isOnline()); const [offlinePending, setOfflinePending] = useStateA(() => window.api.hasOfflineQueue()); const [syncing, setSyncing] = useStateA(false); const [toastNode, showToast] = useToast(); const counts = { quarantine: (state.quarantineLog || []).length, bunker: (state.bunker || []).length, missions: (state.firingLog || []).length, }; // Listen for sync conflict events from api.js useEffectA(() => { const handler = () => showToast('⚠ סנכרון: נמצאה גרסה חדשה יותר — התצוגה עודכנה'); window.addEventListener('arty:sync-conflict', handler); return () => window.removeEventListener('arty:sync-conflict', handler); }, [showToast]); // Listen for online/offline status changes useEffectA(() => { const onStatus = (e) => setIsOnline(e.detail.online); const onQueued = () => setOfflinePending(true); const onBackOnline = () => { // Back online and there's a queue — show a prompt but don't auto-sync setIsOnline(true); showToast('חיבור שוחזר — לחץ "סנכרן שינויים" לשמירה'); }; window.addEventListener('arty:online-status', onStatus); window.addEventListener('arty:queued-offline', onQueued); window.addEventListener('arty:back-online', onBackOnline); return () => { window.removeEventListener('arty:online-status', onStatus); window.removeEventListener('arty:queued-offline', onQueued); window.removeEventListener('arty:back-online', onBackOnline); }; }, [showToast]); const go = id => { setView(id); setDrawer(false); }; const addPallet = (ammo, newTypes) => { dispatch({ type: 'ADD_PALLET', ammo, newTypes }); setModal(null); showToast('המשטח נקלט בהצלחה'); }; // ── Export (admin only — uses cached token) ────────────── const handleExport = async () => { try { const d = await window.api.exportState(); const blob = new Blob([JSON.stringify(d, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `arty-ammo-backup-${new Date().toISOString().slice(0, 10)}.json`; a.click(); URL.revokeObjectURL(url); showToast('ייצוא הצליח'); } catch (e) { showToast('שגיאה בייצוא — בדוק הרשאות', 'error'); } }; // ── Import (admin-gated) ────────────────────────────────── const handleImportFile = (e) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = ev => { try { setImportFile(JSON.parse(ev.target.result)); setAdminModal('import'); } catch { showToast('קובץ לא תקין', 'error'); } }; reader.readAsText(file); }; const handleImport = async () => { if (!importFile) return; const payload = importFile.data || importFile; const ver = importFile.version || 0; try { await window.api.importState(payload, ver); dispatch({ type: 'IMPORT_STATE', payload: applyMigrationsA ? applyMigrationsA(payload) : payload }); setAdminModal(null); setImportFile(null); showToast('ייבוא הצליח — המערכת עודכנה'); } catch { showToast('שגיאה בייבוא — בדוק הרשאות', 'error'); } }; const handleResetStep1 = () => { // Admin already authenticated — just confirm setAdminModal(null); setResetConfirm(true); }; const handleResetConfirmed = async () => { const freshState = buildInitA(); try { const r = await fetch(window.api.base() + '/state', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + window.api._token }, body: JSON.stringify({ data: freshState, version: null }), }); if (!r.ok) throw new Error('save failed: ' + r.status); window.api._version = 1; } catch (e) { console.error('[reset] DB write failed:', e.message); showToast('שגיאה בכתיבה ל-DB — נסה שוב', 'error'); setResetConfirm(false); return; } dispatch({ type: 'RESET_STATE' }); try { localStorage.clear(); } catch {} setResetConfirm(false); showToast('המערכת אופסה בהצלחה — כל הנתונים נמחקו'); }; // ── Manual sync of offline queue ───────────────────────── const handleSync = async () => { if (syncing || !isOnline) return; setSyncing(true); try { const result = await window.api.syncOfflineQueue(); if (result.synced) { setOfflinePending(false); showToast('✓ השינויים סונכרנו בהצלחה'); } else if (result.reason === 'still_offline') { showToast('עדיין אין חיבור — נסה שוב', 'error'); } else if (result.reason === 'conflict_server_wins') { setOfflinePending(false); showToast('⚠ קונפליקט — המערכת עודכנה מהשרת'); } else { showToast('שגיאה בסנכרון — נסה שוב', 'error'); } } finally { setSyncing(false); } }; return (
{/* ── Offline banner (topbar overlay) ── */} {!isOnline && (
⚠ אין חיבור לרשת — שינויים נשמרים מקומית ויסונכרנו בחזרת החיבור
)} {/* ── Rail ── */}
setDrawer(false)} /> {/* ── Main ── */}

{VIEW_TITLES[view]}

משימת אש אחרונה: {state.lastFireMissionId || '—'}
{window.api?.canEdit() && <> } {window.api?.isViewer() && ( 👁 מצב צפייה בלבד )}
{view === 'overview' && } {view === 'teams' && } {view === 'bunker' && setModal('pallet')} />} {view === 'barrel' && } {view === 'missions' && } {view === 'transfers' && } {view === 'quarantine' && } {view === 'catalog' && }
{/* ── Modals ── */} {modal === 'fire' && setModal(null)} />} {modal === 'pallet' && setModal(null)} onSubmit={addPallet} />} {modal === 'transfer' && setModal(null)} />} {modal === 'stock' && setModal(null)} />} {/* ── Admin modals — no password re-entry (token cached in session) ── */} {adminModal === 'import' && importFile && ( ייבוא יחליף את כל נתוני המערכת. הקלד RESET לאישור.} onConfirm={handleImport} onCancel={() => { setAdminModal(null); setImportFile(null); }} /> )} {adminModal === 'reset' && setAdminModal(null)} />} {resetConfirm && setResetConfirm(false)} />} {toastNode}
); } /* ── Admin password modal ────────────────────────────────── */ function AdminGate({ title, onSuccess, onCancel }) { const [code, setCode] = useStateA(''); const [err, setErr] = useStateA(''); const [working, setWorking] = useStateA(false); const attempt = async () => { if (!code || working) return; setWorking(true); setErr(''); try { await onSuccess(code); } catch { setErr('קוד מנהל שגוי ⛔'); setCode(''); } finally { setWorking(false); } }; return (
{ if (e.target === e.currentTarget) onCancel(); }}>
🔐 {title}
נדרש קוד מנהל (ADMIN_PASSWORD)
{err &&
{err}
} setCode(e.target.value)} onKeyDown={e => e.key === 'Enter' && attempt()} placeholder="הקלד קוד מנהל..." autoFocus style={{ textAlign: 'center', fontSize: '1rem', letterSpacing: '.15em' }} />
); } /* ── Full reset second-confirmation modal ────────────────── */ function ResetConfirmModal({ onConfirm, onCancel, title, message }) { const [typed, setTyped] = useStateA(''); const ok = typed.trim() === 'RESET'; return (
{ if (e.target === e.currentTarget) onCancel(); }}>
⚠ {title || 'אישור סופי — איפוס מלא'}
{message || (<>כל הנתונים יימחקו לצמיתות מה-DB.
מלאי, משימות, תנועות, פסולים, בלאי קנים, קטלוג.
פעולה זו אינה הפיכה.)}
setTyped(e.target.value)} placeholder="RESET" autoFocus style={{ textAlign: 'center', fontFamily: 'monospace', fontSize: '1.1rem' }} />
); } /* ── Root: config + auth + state wiring ─────────────────── */ function AuthedApp({ theme, onToggleTheme, onLogout }) { const [loadingCloud, setLoadingCloud] = useStateA(true); const [state, dispatch] = useReducer(reducerA, null, () => buildInitA()); useEffectA(() => { window.__appDispatch = dispatch; return () => { window.__appDispatch = null; }; }, [dispatch]); const cloudLoaded = useRefA(false); const firstMount = useRefA(true); useEffectA(() => { window.__syncConflictToast = () => window.dispatchEvent(new CustomEvent('arty:sync-conflict')); return () => { window.__syncConflictToast = null; }; }, []); useEffectA(() => { let alive = true; (async () => { try { const r = await window.api.loadState(); if (alive && r && r.data) dispatch({ type: 'IMPORT_STATE', payload: applyMigrationsA(r.data) }); } catch {} finally { if (alive) { cloudLoaded.current = true; setLoadingCloud(false); window.api.startPolling(30000); } } })(); return () => { alive = false; window.api.stopPolling(); }; }, []); useEffectA(() => { if (firstMount.current) { firstMount.current = false; return; } if (!cloudLoaded.current) return; // Only operators and admins save — viewers are read-only if (window.api.canEdit()) saveStateA(state); }, [state]); return ; } function Root() { const [ready, setReady] = useStateA(false); const [authed, setAuthed] = useStateA(false); const [sessionExpired, setSessionExpired] = useStateA(false); const [theme, setTheme] = useStateA(() => { try { return localStorage.getItem('arty_theme') || 'dark'; } catch { return 'dark'; } }); useEffectA(() => { document.documentElement.setAttribute('data-theme', theme); try { localStorage.setItem('arty_theme', theme); } catch {} }, [theme]); const toggleTheme = () => setTheme(t => t === 'dark' ? 'light' : 'dark'); useEffectA(() => { (async () => { try { await window.loadConfig(); } catch {} const session = window.api.restoreSession(); if (session) setAuthed(true); setReady(true); })(); }, []); // Handle midnight token expiry — show a "session expired, please log in again" notice useEffectA(() => { const handler = () => { setSessionExpired(true); setAuthed(false); }; window.addEventListener('arty:session-expired', handler); return () => window.removeEventListener('arty:session-expired', handler); }, []); const logout = () => { window.api.stopPolling(); window.api.logout(); try { localStorage.clear(); } catch {} setAuthed(false); setSessionExpired(false); }; if (!ready) return
טוען מערכת…
; if (!authed) return { setAuthed(true); setSessionExpired(false); }} sessionExpired={sessionExpired} />; return ; } window.App = App; window.Root = Root; ReactDOM.createRoot(document.getElementById('root')).render(); })();