import { useState, useEffect, useRef, useCallback } from "react"; import { createPortal } from "react-dom"; import { useAuth } from "../auth/AuthContext"; import api from "../api/client"; // ── Constants ───────────────────────────────────────────────────────────────── const KNOWN_BOARD_TYPES = [ { value: "vesper", label: "Vesper", family: "vesper" }, { value: "vesper_plus", label: "Vesper+", family: "vesper" }, { value: "vesper_pro", label: "Vesper Pro", family: "vesper" }, { value: "agnus", label: "Agnus", family: "agnus" }, { value: "agnus_mini", label: "Agnus Mini", family: "agnus" }, { value: "chronos", label: "Chronos", family: "chronos" }, { value: "chronos_pro", label: "Chronos Pro", family: "chronos" }, ]; const BOARD_LABELS = Object.fromEntries(KNOWN_BOARD_TYPES.map((b) => [b.value, b.label])); const FAMILY_ACCENT = { vesper: "#3b82f6", agnus: "#f59e0b", chronos: "#ef4444", bespoke: "#a855f7", }; // ── Helpers ─────────────────────────────────────────────────────────────────── function formatBytes(bytes) { if (!bytes) return "—"; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(2)} MB`; } function formatDate(iso) { if (!iso) return null; try { return new Date(iso).toLocaleString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } catch { return iso; } } function boardLabel(hw_type, is_bespoke) { if (is_bespoke) return hw_type; return BOARD_LABELS[hw_type] || hw_type; } function familyOf(hw_type, is_bespoke) { if (is_bespoke) return "bespoke"; const bt = KNOWN_BOARD_TYPES.find((b) => b.value === hw_type); return bt?.family ?? "vesper"; } // ── Icons ───────────────────────────────────────────────────────────────────── function IconCheck() { return ( ); } function IconMissing() { return ( ); } function IconTrash() { return ( ); } function IconUpload() { return ( ); } function IconNote() { return ( ); } function IconRefresh() { return ( ); } // ── Asset file pill (used inside each board card) ──────────────────────────── function AssetPill({ label, info, canDelete, onDelete, onUpload }) { const exists = info?.exists; return (
{exists ? : }
{label}
{exists ? (
{formatBytes(info.size_bytes)} · {formatDate(info.uploaded_at)}
) : (
Not uploaded
)}
{exists && canDelete && ( )}
); } // ── Inline Upload Modal ─────────────────────────────────────────────────────── function UploadModal({ hwType, assetName, onClose, onSaved }) { const [file, setFile] = useState(null); const [uploading, setUploading] = useState(false); const [error, setError] = useState(""); const fileInputRef = useRef(null); const handleUpload = async () => { if (!file) return; setError(""); setUploading(true); try { const formData = new FormData(); formData.append("file", file); const token = localStorage.getItem("access_token"); const res = await fetch(`/api/manufacturing/flash-assets/${hwType}/${assetName}`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, body: formData, }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || "Upload failed"); } onSaved(); } catch (err) { setError(err.message); } finally { setUploading(false); } }; return (
e.stopPropagation()} >

Upload {assetName}

Board: {hwType} — overwrites existing file

{error && (
{error}
)}
fileInputRef.current?.click()} onDragOver={(e) => e.preventDefault()} onDrop={(e) => { e.preventDefault(); const f = e.dataTransfer.files[0]; if (f && f.name.endsWith(".bin")) setFile(f); else if (f) setError("Only .bin files are accepted."); }} style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: "0.5rem", padding: "2rem 1rem", border: `2px dashed ${file ? "var(--btn-primary)" : "var(--border-input)"}`, borderRadius: "0.625rem", backgroundColor: file ? "var(--badge-blue-bg)" : "var(--bg-input)", cursor: "pointer", transition: "all 0.15s ease", }} > { const f = e.target.files[0]; if (f && !f.name.endsWith(".bin")) { setError("Only .bin files are accepted."); return; } setFile(f || null); setError(""); }} style={{ display: "none" }} /> {file ? ( <> {file.name} {formatBytes(file.size)} ) : ( <> Click or drop {assetName} )}
); } // ── Note Editor Modal ───────────────────────────────────────────────────────── function NoteModal({ hwType, currentNote, onClose, onSaved }) { const [note, setNote] = useState(currentNote || ""); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); const handleSave = async () => { setSaving(true); setError(""); try { await api.put(`/manufacturing/flash-assets/${hwType}/note`, { note }); onSaved(note); } catch (err) { setError(err.message || "Failed to save note"); } finally { setSaving(false); } }; return (
e.stopPropagation()} >

Asset Note

{hwType} — stored as note.txt alongside the binaries

{error && (
{error}
)}