feat(ToolsPage): add firmware management overlay with installation functionality
This commit is contained in:
parent
f82b669fe2
commit
63d2ff9f69
|
|
@ -1,12 +1,145 @@
|
||||||
import { Wrench, Download, Upload, RotateCcw, Database, FileText, HardDrive } from 'lucide-react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Wrench, Download, Upload, RotateCcw, Database, FileText, HardDrive, X, Loader2, Cpu, ChevronRight, AlertCircle } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { getFileContents, putFileContents, humanFileSize } from '../webdav';
|
||||||
|
|
||||||
interface ToolsPageProps {
|
interface ToolsPageProps {
|
||||||
config: any;
|
config: any;
|
||||||
setConfig: (config: any) => void;
|
setConfig: (config: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FirmwareEntry {
|
||||||
|
version: string;
|
||||||
|
date: string;
|
||||||
|
description: string;
|
||||||
|
url: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FirmwareOverlay({ onClose }: { onClose: () => void }) {
|
||||||
|
const [entries, setEntries] = useState<FirmwareEntry[] | null>(null);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
const [installing, setInstalling] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getFileContents('/.sys/firmware.json')
|
||||||
|
.then(async (blob) => {
|
||||||
|
const parsed = JSON.parse(await blob.text());
|
||||||
|
if (Array.isArray(parsed)) {
|
||||||
|
setEntries(parsed);
|
||||||
|
} else {
|
||||||
|
setLoadError('Unexpected firmware manifest format.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e: any) => setLoadError(e?.message || 'Failed to load firmware list.'));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const install = async (entry: FirmwareEntry) => {
|
||||||
|
setInstalling(entry.url);
|
||||||
|
try {
|
||||||
|
let data: ArrayBuffer;
|
||||||
|
if (/^https?:\/\//.test(entry.url)) {
|
||||||
|
const r = await fetch(entry.url);
|
||||||
|
if (!r.ok) throw new Error(`Download failed: ${r.status} ${r.statusText}`);
|
||||||
|
data = await r.arrayBuffer();
|
||||||
|
} else {
|
||||||
|
const blob = await getFileContents(entry.url);
|
||||||
|
data = await blob.arrayBuffer();
|
||||||
|
}
|
||||||
|
await putFileContents('/sd/.bin', data);
|
||||||
|
toast.success(`Firmware ${entry.version} saved to /sd/.bin`);
|
||||||
|
onClose();
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error(`Install failed: ${e?.message || String(e)}`);
|
||||||
|
} finally {
|
||||||
|
setInstalling(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-white z-50 flex flex-col">
|
||||||
|
<div className="flex items-center justify-between p-4 border-b border-neutral-200">
|
||||||
|
<div>
|
||||||
|
<h2 className="font-semibold text-lg">Change Firmware</h2>
|
||||||
|
<p className="text-xs text-neutral-500 mt-0.5">Selected firmware will be saved to /sd/.bin</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={onClose} className="p-2 -m-2 hover:bg-neutral-100 rounded-lg">
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto p-4 space-y-3">
|
||||||
|
{entries === null && !loadError && (
|
||||||
|
<div className="flex flex-col items-center justify-center gap-3 py-16 text-neutral-500">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin" />
|
||||||
|
<span className="text-sm">Loading firmware list…</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{loadError && (
|
||||||
|
<div className="flex items-start gap-3 p-4 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
|
||||||
|
<AlertCircle className="w-5 h-5 flex-shrink-0 mt-0.5" />
|
||||||
|
<span>{loadError}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entries && entries.length === 0 && (
|
||||||
|
<div className="text-center py-16 text-sm text-neutral-500">
|
||||||
|
No firmware updates available.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{entries && entries.map((entry) => {
|
||||||
|
const isInstalling = installing === entry.url;
|
||||||
|
const busy = installing !== null;
|
||||||
|
return (
|
||||||
|
<div key={entry.url} className="border border-neutral-200 rounded-lg p-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="flex flex-wrap items-center gap-2 mb-1">
|
||||||
|
<span className="font-semibold text-neutral-900">{entry.version}</span>
|
||||||
|
<span className="text-xs text-neutral-400">{entry.date}</span>
|
||||||
|
{entry.size != null && (
|
||||||
|
<span className="text-xs text-neutral-400">· {humanFileSize(entry.size)}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-neutral-600 leading-snug">{entry.description}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => void install(entry)}
|
||||||
|
disabled={busy}
|
||||||
|
className={`flex-shrink-0 flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-medium transition ${
|
||||||
|
isInstalling
|
||||||
|
? 'bg-blue-100 text-blue-600'
|
||||||
|
: busy
|
||||||
|
? 'bg-neutral-100 text-neutral-400 cursor-not-allowed'
|
||||||
|
: 'bg-blue-600 text-white hover:bg-blue-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isInstalling ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||||
|
Installing
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Download className="w-3.5 h-3.5" />
|
||||||
|
Install
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function ToolsPage({ config }: ToolsPageProps) {
|
export default function ToolsPage({ config }: ToolsPageProps) {
|
||||||
|
const [showFirmware, setShowFirmware] = useState(false);
|
||||||
|
|
||||||
const handleBackup = () => {
|
const handleBackup = () => {
|
||||||
const dataStr = JSON.stringify(config, null, 2);
|
const dataStr = JSON.stringify(config, null, 2);
|
||||||
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
|
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
|
||||||
|
|
@ -140,32 +273,6 @@ export default function ToolsPage({ config }: ToolsPageProps) {
|
||||||
<h2 className="text-sm text-neutral-500 pt-4">System</h2>
|
<h2 className="text-sm text-neutral-500 pt-4">System</h2>
|
||||||
|
|
||||||
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
|
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">
|
||||||
<button
|
|
||||||
onClick={handleSystemUpdate}
|
|
||||||
className="w-full p-4 flex items-center gap-3 hover:bg-neutral-50 text-left"
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
|
|
||||||
<Download className="w-5 h-5 text-purple-600" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="font-medium">System Update</div>
|
|
||||||
<div className="text-sm text-neutral-500">Check for firmware updates</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={handleExportLogs}
|
|
||||||
className="w-full p-4 flex items-center gap-3 hover:bg-neutral-50 text-left"
|
|
||||||
>
|
|
||||||
<div className="w-10 h-10 bg-neutral-100 rounded-lg flex items-center justify-center">
|
|
||||||
<FileText className="w-5 h-5 text-neutral-600" />
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="font-medium">Export System Logs</div>
|
|
||||||
<div className="text-sm text-neutral-500">Download diagnostic logs</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<div className="w-10 h-10 bg-neutral-100 rounded-lg flex items-center justify-center">
|
<div className="w-10 h-10 bg-neutral-100 rounded-lg flex items-center justify-center">
|
||||||
|
|
@ -190,7 +297,50 @@ export default function ToolsPage({ config }: ToolsPageProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleSystemUpdate}
|
||||||
|
className="w-full p-4 flex items-center gap-3 hover:bg-neutral-50 text-left"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||||
|
<Download className="w-5 h-5 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium">System Update</div>
|
||||||
|
<div className="text-sm text-neutral-500">Check for firmware updates</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setShowFirmware(true)}
|
||||||
|
className="w-full p-4 flex items-center gap-3 hover:bg-neutral-50 text-left"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
|
||||||
|
<Cpu className="w-5 h-5 text-purple-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium">Change Firmware</div>
|
||||||
|
<div className="text-sm text-neutral-500">Select a firmware version to install</div>
|
||||||
|
</div>
|
||||||
|
<ChevronRight className="w-4 h-4 text-neutral-400" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleExportLogs}
|
||||||
|
className="w-full p-4 flex items-center gap-3 hover:bg-neutral-50 text-left"
|
||||||
|
>
|
||||||
|
<div className="w-10 h-10 bg-neutral-100 rounded-lg flex items-center justify-center">
|
||||||
|
<FileText className="w-5 h-5 text-neutral-600" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium">Export System Logs</div>
|
||||||
|
<div className="text-sm text-neutral-500">Download diagnostic logs</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{showFirmware && <FirmwareOverlay onClose={() => setShowFirmware(false)} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user