update: Added asset upload for bespoke boards

This commit is contained in:
2026-03-31 18:01:32 +03:00
parent 7a5321c097
commit 435aa88e29

View File

@@ -373,6 +373,161 @@ function NoteModal({ hwType, currentNote, onClose, onSaved }) {
); );
} }
// ── Add Bespoke Board Modal ───────────────────────────────────────────────────
function AddBespokeModal({ existingUids, onClose, onSaved }) {
const [uid, setUid] = useState("");
const [bootFile, setBootFile] = useState(null);
const [partFile, setPartFile] = useState(null);
const [uploading, setUploading] = useState(false);
const [error, setError] = useState("");
const bootRef = useRef(null);
const partRef = useRef(null);
const uidSlug = uid.trim().toLowerCase().replace(/\s+/g, "-");
const isValidUid = /^[a-z0-9][a-z0-9._-]{0,126}$/.test(uidSlug);
const isKnown = KNOWN_BOARD_TYPES.some((b) => b.value === uidSlug);
const isDuplicate = existingUids.includes(uidSlug);
const handleSubmit = async (e) => {
e.preventDefault();
if (!isValidUid) { setError("UID must be lowercase alphanumeric with hyphens/dots only."); return; }
if (isKnown) { setError("That name matches a standard board type. Use a unique bespoke UID."); return; }
if (isDuplicate) { setError("A bespoke board with that UID already exists."); return; }
if (!bootFile && !partFile) { setError("Upload at least one file."); return; }
setError(""); setUploading(true);
const token = localStorage.getItem("access_token");
try {
for (const [asset, file] of [["bootloader.bin", bootFile], ["partitions.bin", partFile]]) {
if (!file) continue;
const fd = new FormData();
fd.append("file", file);
const res = await fetch(`/api/manufacturing/flash-assets/${uidSlug}/${asset}`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: fd,
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || `Failed to upload ${asset}`);
}
}
onSaved();
} catch (err) {
setError(err.message);
} finally {
setUploading(false);
}
};
const FilePicker = ({ label, file, setFile, inputRef }) => (
<div>
<label className="block text-xs font-medium mb-1" style={{ color: "var(--text-muted)" }}>{label}</label>
<div
onClick={() => inputRef.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", alignItems: "center", gap: "0.5rem",
padding: "0.5rem 0.75rem",
border: `2px dashed ${file ? "var(--btn-primary)" : "var(--border-input)"}`,
borderRadius: "0.5rem",
backgroundColor: file ? "var(--badge-blue-bg)" : "var(--bg-input)",
cursor: "pointer", transition: "all 0.15s ease",
}}
>
<input ref={inputRef} type="file" accept=".bin" style={{ display: "none" }}
onChange={(e) => {
const f = e.target.files[0];
if (f && !f.name.endsWith(".bin")) { setError("Only .bin files are accepted."); return; }
setFile(f || null); setError("");
}}
/>
<span style={{ color: file ? "var(--btn-primary)" : "var(--text-muted)", flexShrink: 0 }}>
{file ? <IconCheck /> : <IconUpload />}
</span>
<span className="font-mono" style={{ fontSize: "0.72rem", color: file ? "var(--badge-blue-text)" : "var(--text-muted)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{file ? `${file.name} (${formatBytes(file.size)})` : `click or drop ${label}`}
</span>
</div>
</div>
);
return (
<div
className="fixed inset-0 flex items-center justify-center z-50"
style={{ backgroundColor: "rgba(0,0,0,0.6)" }}
onClick={onClose}
>
<div
className="rounded-lg border w-full mx-4 flex flex-col"
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)", maxWidth: "480px" }}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between px-5 pt-4 pb-3" style={{ borderBottom: "1px solid var(--border-secondary)" }}>
<div>
<h3 className="text-sm font-semibold" style={{ color: "var(--text-heading)" }}>Add Bespoke Board</h3>
<p className="text-xs mt-0.5" style={{ color: "var(--text-muted)" }}>
Create a new bespoke flash asset set with a unique UID.
</p>
</div>
<button onClick={onClose} className="text-lg leading-none hover:opacity-70 cursor-pointer" style={{ color: "var(--text-muted)" }}></button>
</div>
<form onSubmit={handleSubmit}>
<div className="px-5 py-4" style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
{error && (
<div className="text-xs rounded-md p-3 border" style={{ backgroundColor: "var(--danger-bg)", borderColor: "var(--danger)", color: "var(--danger-text)" }}>
{error}
</div>
)}
<div>
<label className="block text-xs font-medium mb-1" style={{ color: "var(--text-muted)" }}>
Bespoke UID <span style={{ color: "var(--danger-text)" }}>*</span>
</label>
<input
type="text"
value={uid}
onChange={(e) => { setUid(e.target.value); setError(""); }}
placeholder="e.g. client-athens-v1"
required
className="w-full px-3 py-2 rounded-md text-sm border font-mono"
style={{ backgroundColor: "var(--bg-input)", borderColor: "var(--border-input)", color: "var(--text-primary)" }}
/>
{uidSlug && (
<p className="text-xs mt-1 font-mono" style={{ color: isValidUid && !isKnown && !isDuplicate ? "var(--text-muted)" : "var(--danger-text)" }}>
{isDuplicate ? "Already exists" : isKnown ? "Matches a standard board type" : !isValidUid ? "Invalid format" : `${uidSlug}`}
</p>
)}
</div>
<FilePicker label="bootloader.bin" file={bootFile} setFile={setBootFile} inputRef={bootRef} />
<FilePicker label="partitions.bin" file={partFile} setFile={setPartFile} inputRef={partRef} />
</div>
<div className="flex items-center justify-end gap-3 px-5 py-3" style={{ borderTop: "1px solid var(--border-secondary)" }}>
<button type="button" onClick={onClose} className="px-4 py-2 text-sm rounded-md hover:opacity-80 cursor-pointer" style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)" }}>
Cancel
</button>
<button
type="submit"
disabled={uploading || !uidSlug}
className="px-5 py-2 text-sm rounded-md font-medium hover:opacity-90 transition-opacity cursor-pointer disabled:opacity-50"
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
>
{uploading ? "Uploading…" : "Add Board"}
</button>
</div>
</form>
</div>
</div>
);
}
// ── Delete Confirm Modal ────────────────────────────────────────────────────── // ── Delete Confirm Modal ──────────────────────────────────────────────────────
function DeleteConfirmModal({ hwType, assetName, onClose, onConfirmed }) { function DeleteConfirmModal({ hwType, assetName, onClose, onConfirmed }) {
@@ -548,9 +703,11 @@ export default function FlashAssetManager({ onClose }) {
const [filter, setFilter] = useState("all"); // "all" | "ready" | "partial" | "missing" const [filter, setFilter] = useState("all"); // "all" | "ready" | "partial" | "missing"
// Modal state // Modal state
const [uploadTarget, setUploadTarget] = useState(null); // { hwType, assetName } const [uploadTarget, setUploadTarget] = useState(null); // { hwType, assetName }
const [deleteTarget, setDeleteTarget] = useState(null); // { hwType, assetName } const [deleteTarget, setDeleteTarget] = useState(null); // { hwType, assetName }
const [noteTarget, setNoteTarget] = useState(null); // entry const [noteTarget, setNoteTarget] = useState(null); // entry
const [showAddBespoke, setShowAddBespoke] = useState(false);
const canAdd = hasPermission("manufacturing", "add");
const fetchAssets = useCallback(async () => { const fetchAssets = useCallback(async () => {
setLoading(true); setError(""); setLoading(true); setError("");
@@ -653,6 +810,15 @@ export default function FlashAssetManager({ onClose }) {
<IconRefresh /> <IconRefresh />
Refresh Refresh
</button> </button>
{canAdd && (
<button
onClick={() => setShowAddBespoke(true)}
className="flex items-center gap-1.5 px-3 py-2 text-sm rounded-md font-medium cursor-pointer hover:opacity-90 transition-opacity"
style={{ backgroundColor: "#2a0a3a", color: "#a855f7", border: "1px solid #a855f7" }}
>
+ Bespoke Board
</button>
)}
<button <button
onClick={onClose} onClick={onClose}
className="text-lg leading-none hover:opacity-70 cursor-pointer" className="text-lg leading-none hover:opacity-70 cursor-pointer"
@@ -788,6 +954,14 @@ export default function FlashAssetManager({ onClose }) {
}} }}
/> />
)} )}
{showAddBespoke && (
<AddBespokeModal
existingUids={assets.filter((e) => e.is_bespoke).map((e) => e.hw_type)}
onClose={() => setShowAddBespoke(false)}
onSaved={() => { setShowAddBespoke(false); fetchAssets(); }}
/>
)}
</div> </div>
); );
} }