···
interface PlcUpdateStep {
+
status: "pending" | "in-progress" | "verifying" | "completed" | "error";
···
const [hasStarted, setHasStarted] = useState(false);
const [steps, setSteps] = useState<PlcUpdateStep[]>([
{ name: "Generate PLC key", status: "pending" },
+
{ name: "Start PLC update", status: "pending" },
+
{ name: "Complete PLC update", status: "pending" },
const [generatedKey, setGeneratedKey] = useState<string>("");
+
const [keyJson, setKeyJson] = useState<any>(null);
+
const [emailToken, setEmailToken] = useState<string>("");
const [updateResult, setUpdateResult] = useState<string>("");
+
const [showDownload, setShowDownload] = useState(false);
+
const [showKeyInfo, setShowKeyInfo] = useState(false);
const updateStepStatus = (
status: PlcUpdateStep["status"],
+
`Updating step ${index} to ${status}${
+
error ? ` with error: ${error}` : ""
prevSteps.map((step, i) =>
+
? { ...step, status, error }
+
? { ...step, status: "pending", error: undefined }
const handleStart = () => {
+
// Automatically start the first step
+
const getStepDisplayName = (step: PlcUpdateStep, index: number) => {
+
if (step.status === "completed") {
+
return "PLC Key Generated";
+
return "PLC Update Started";
+
return "PLC Update Completed";
+
if (step.status === "in-progress") {
+
return "Generating PLC key...";
+
return "Starting PLC update...";
+
"Enter the token sent to your email to complete PLC update"
+
: "Completing PLC update...";
+
if (step.status === "verifying") {
+
return "Verifying key generation...";
+
return "Verifying PLC update start...";
+
return "Verifying PLC update completion...";
const handleGenerateKey = async () => {
updateStepStatus(0, "in-progress");
+
setShowDownload(false);
const res = await fetch("/api/plc/keys");
const text = await res.text();
···
throw new Error("Invalid response from /api/plc/keys");
+
if (!data.publicKeyDid || !data.privateKeyHex) {
+
throw new Error("Key generation failed: missing key data");
+
setGeneratedKey(data.publicKeyDid);
updateStepStatus(0, "completed");
+
// Auto-download the key
+
console.log("Attempting auto-download with keyJson:", keyJson);
+
// Auto-continue to next step with the generated key
+
handleStartPlcUpdate(data.publicKeyDid);
···
+
const handleStartPlcUpdate = async (keyToUse?: string) => {
+
const key = keyToUse || generatedKey;
+
console.log("No key generated yet", { key, generatedKey });
+
updateStepStatus(1, "error", "No key generated yet");
updateStepStatus(1, "in-progress");
const res = await fetch("/api/plc/update", {
headers: { "Content-Type": "application/json" },
+
body: JSON.stringify({ key: key }),
const text = await res.text();
const json = JSON.parse(text);
+
throw new Error(json.message || "Failed to start PLC update");
+
throw new Error(text || "Failed to start PLC update");
+
// Update step name to prompt for token
+
setSteps((prevSteps) =>
+
prevSteps.map((step, i) =>
+
name: "Enter the token sent to your email to complete PLC update",
updateStepStatus(1, "completed");
···
error instanceof Error ? error.message : String(error)
+
const handleCompletePlcUpdate = async () => {
+
updateStepStatus(2, "error", "Please enter the email token");
+
updateStepStatus(2, "in-progress");
+
const res = await fetch(
+
`/api/plc/update/complete?token=${encodeURIComponent(emailToken)}`,
+
headers: { "Content-Type": "application/json" },
+
const text = await res.text();
+
const json = JSON.parse(text);
+
throw new Error(json.message || "Failed to complete PLC update");
+
throw new Error(text || "Failed to complete PLC update");
+
data = JSON.parse(text);
+
throw new Error(data.message || "PLC update failed");
+
throw new Error("Invalid response from server");
+
setUpdateResult("PLC update completed successfully!");
+
updateStepStatus(2, "completed");
+
error instanceof Error ? error.message : String(error)
setUpdateResult(error instanceof Error ? error.message : String(error));
+
const handleDownload = () => {
+
console.log("handleDownload called with keyJson:", keyJson);
+
console.error("No key JSON to download");
+
const jsonString = JSON.stringify(keyJson, null, 2);
+
console.log("JSON string to download:", jsonString);
+
const blob = new Blob([jsonString], {
+
type: "application/json",
+
const url = URL.createObjectURL(blob);
+
const a = document.createElement("a");
+
a.download = `plc-key-${keyJson.publicKeyDid || "unknown"}.json`;
+
a.style.display = "none";
+
document.body.appendChild(a);
+
console.log("Download link created, clicking...");
+
document.body.removeChild(a);
+
URL.revokeObjectURL(url);
+
console.log("Key downloaded successfully:", keyJson.publicKeyDid);
+
console.error("Download failed:", error);
+
const getStepIcon = (status: PlcUpdateStep["status"]) => {
+
<div class="w-8 h-8 rounded-full border-2 border-gray-300 dark:border-gray-600 flex items-center justify-center">
+
<div class="w-3 h-3 rounded-full bg-gray-300 dark:bg-gray-600" />
+
<div class="w-8 h-8 rounded-full border-2 border-blue-500 border-t-transparent animate-spin flex items-center justify-center">
+
<div class="w-3 h-3 rounded-full bg-blue-500" />
+
<div class="w-8 h-8 rounded-full border-2 border-yellow-500 border-t-transparent animate-spin flex items-center justify-center">
+
<div class="w-3 h-3 rounded-full bg-yellow-500" />
+
<div class="w-8 h-8 rounded-full bg-green-500 flex items-center justify-center">
+
class="w-5 h-5 text-white"
+
stroke-linejoin="round"
+
<div class="w-8 h-8 rounded-full bg-red-500 flex items-center justify-center">
+
class="w-5 h-5 text-white"
+
stroke-linejoin="round"
+
d="M6 18L18 6M6 6l12 12"
+
const getStepClasses = (status: PlcUpdateStep["status"]) => {
+
"flex items-center space-x-3 p-4 rounded-lg transition-colors duration-200";
+
return `${baseClasses} bg-gray-50 dark:bg-gray-800`;
+
return `${baseClasses} bg-blue-50 dark:bg-blue-900`;
+
return `${baseClasses} bg-yellow-50 dark:bg-yellow-900`;
+
return `${baseClasses} bg-green-50 dark:bg-green-900`;
+
return `${baseClasses} bg-red-50 dark:bg-red-900`;
···
• Generate a new PLC key with cryptographic signature verification
+
<p>• Start PLC update process (sends email with token)</p>
+
<p>• Complete PLC update using email token</p>
<p>• All operations require authentication</p>
···
+
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
+
{steps.map((step, index) => (
+
<div key={step.name} class={getStepClasses(step.status)}>
+
{getStepIcon(step.status)}
+
step.status === "error"
+
? "text-red-900 dark:text-red-200"
+
: step.status === "completed"
+
? "text-green-900 dark:text-green-200"
+
: step.status === "in-progress"
+
? "text-blue-900 dark:text-blue-200"
+
: "text-gray-900 dark:text-gray-200"
+
{getStepDisplayName(step, index)}
+
<p class="text-sm text-red-600 dark:text-red-400 mt-1">
+
const err = JSON.parse(step.error);
+
return err.message || step.error;
+
{index === 1 && step.status === "completed" && (
+
onClick={() => handleStartPlcUpdate()}
+
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors duration-200"
+
step.status === "in-progress" &&
+
"Enter the token sent to your email to complete PLC update" && (
+
<div class="mt-4 space-y-4">
+
<p class="text-sm text-blue-800 dark:text-blue-200">
+
Please check your email for the PLC update token and enter
+
<div class="flex space-x-2">
+
onChange={(e) => setEmailToken(e.currentTarget.value)}
+
placeholder="Enter token"
+
class="flex-1 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:focus:border-blue-400 dark:focus:ring-blue-400"
+
onClick={handleCompletePlcUpdate}
+
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors duration-200"
+
{/* Key Information Section - Collapsible at bottom */}
+
<div class="border border-gray-200 dark:border-gray-700 rounded-lg">
+
onClick={() => setShowKeyInfo(!showKeyInfo)}
+
class="w-full p-4 text-left bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-t-lg flex items-center justify-between"
+
<span class="font-medium text-gray-900 dark:text-gray-100">
+
Generated Key Information
+
class={`w-5 h-5 text-gray-500 transition-transform ${
+
showKeyInfo ? "rotate-180" : ""
+
stroke-linejoin="round"
+
<div class="p-4 bg-white dark:bg-gray-900 rounded-b-lg">
+
<div class="space-y-3 text-sm text-gray-700 dark:text-gray-300">
+
<b>Key type:</b> {keyJson.keyType}
+
<b>Public key (did:key):</b>{" "}
+
<span class="break-all font-mono">
+
<b>Private key (hex):</b>{" "}
+
<span class="break-all font-mono">
+
{keyJson.privateKeyHex}
+
<b>Private key (multikey):</b>{" "}
+
<span class="break-all font-mono">
+
{keyJson.privateKeyMultikey}
+
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md text-sm"
+
onClick={handleDownload}
+
{steps[2].status === "completed" && (
+
<div class="p-4 bg-green-50 dark:bg-green-900 rounded-lg border-2 border-green-200 dark:border-green-800">
+
<p class="text-sm text-green-800 dark:text-green-200">
+
PLC update completed successfully! You can now close this page.
+
const response = await fetch("/api/logout", {
+
credentials: "include",
+
throw new Error("Logout failed");
+
globalThis.location.href = "/";
+
console.error("Failed to logout:", error);
+
class="mt-4 mr-4 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200"
+
href="https://ko-fi.com/knotbin"
+
class="mt-4 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-md transition-colors duration-200"