diff --git a/CLAUDE.md b/CLAUDE.md index f56e49e..8fa761b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -248,5 +248,6 @@ PRs that modify `crates/tyutool-cli/src/main.rs` (command definitions) without u ### Testing - Test files live next to their source, same name with `.test.ts` suffix; Rust uses inline `#[cfg(test)] mod tests` +- **Before creating a test file:** run `ls` in the source file's directory to check whether a co-located `.test.ts` already exists. If it does, append to it — never create a parallel file with suffixes like `-extended`, `-v2`, etc. - Pure logic (utility functions, type conversions) must have unit tests; Vue components and stores as needed - Frontend tests run in the `node` environment — no DOM diff --git a/Cargo.lock b/Cargo.lock index b7e4929..7c649a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5874,7 +5874,7 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "tyutool-cli" -version = "3.0.11" +version = "3.0.14" dependencies = [ "anyhow", "base64 0.22.1", @@ -5905,7 +5905,7 @@ dependencies = [ [[package]] name = "tyutool-core" -version = "3.0.11" +version = "3.0.14" dependencies = [ "crc32fast", "espflash", @@ -5919,7 +5919,7 @@ dependencies = [ [[package]] name = "tyutool_gui" -version = "3.0.11" +version = "3.0.14" dependencies = [ "calamine", "log", diff --git a/crates/tyutool-cli/Cargo.toml b/crates/tyutool-cli/Cargo.toml index 3b19dec..ba1f529 100644 --- a/crates/tyutool-cli/Cargo.toml +++ b/crates/tyutool-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tyutool-cli" -version = "3.0.11" +version = "3.0.14" edition = "2021" description = "tyutool CLI — shared tyutool-core flash API (no Tauri)" diff --git a/crates/tyutool-core/Cargo.toml b/crates/tyutool-core/Cargo.toml index 522338e..89d9d20 100644 --- a/crates/tyutool-core/Cargo.toml +++ b/crates/tyutool-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tyutool-core" -version = "3.0.11" +version = "3.0.14" edition = "2021" description = "Shared flash/erase plugin registry and serial helpers for tyutool CLI and GUI" diff --git a/crates/tyutool-core/src/authorize.rs b/crates/tyutool-core/src/authorize.rs index 4fe6104..8348e7c 100644 --- a/crates/tyutool-core/src/authorize.rs +++ b/crates/tyutool-core/src/authorize.rs @@ -671,7 +671,9 @@ pub enum BatchAuthSlotResult { /// Device already had the exact credentials — nothing written. AlreadyDone { mac: String }, /// Auth on device didn't match but conflict_policy=Skip — nothing written. - Skipped { mac: String }, + /// `existing_uuid` is the UUID already on the device, so the caller can + /// find and confirm that Excel row. + Skipped { mac: String, existing_uuid: String }, /// Operation was cancelled. Cancelled, } @@ -1005,6 +1007,7 @@ where /// The caller pre-allocates `uuid`/`authkey` from an Excel row. On return: /// - `Done`/`AlreadyDone` → caller should confirm the Excel row (mark USED). /// - `Skipped`/`Err`/`Cancelled` → caller should release the Excel row. +#[allow(clippy::too_many_arguments)] pub fn run_batch_auth_slot( port: &str, chip_id: &str, @@ -1079,7 +1082,10 @@ where log::info!( "[batch-auth] skipped port={port} mac={mac} existing_uuid={ex_uuid}" ); - return Ok(BatchAuthSlotResult::Skipped { mac }); + return Ok(BatchAuthSlotResult::Skipped { + mac, + existing_uuid: ex_uuid.clone(), + }); } } } @@ -1175,7 +1181,10 @@ where } if conflict_policy == ConflictPolicy::Skip { log::info!("[batch-auth] skipped (old fw) port={port} mac={mac} existing_uuid={ex_uuid}"); - return Ok(BatchAuthSlotResult::Skipped { mac }); + return Ok(BatchAuthSlotResult::Skipped { + mac, + existing_uuid: ex_uuid.clone(), + }); } } } diff --git a/package.json b/package.json index 4ecb868..dda2a6c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "tyutool", "private": true, - "version": "3.0.11", + "version": "3.0.14", "type": "module", "scripts": { "clean": "node -e \"try { require('fs').rmSync('dist', { recursive: true, force: true }); } catch (e) { if (e && e.code !== 'ENOENT') throw e; }\"", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 21ce390..d6145b6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tyutool_gui" -version = "3.0.11" +version = "3.0.14" description = "tyutool — Tauri shell for tyutool-core (firmware / serial tooling)" authors = ["tyutool contributors"] edition = "2021" diff --git a/src-tauri/src/batch_auth.rs b/src-tauri/src/batch_auth.rs index 6d0f7e2..abadcef 100644 --- a/src-tauri/src/batch_auth.rs +++ b/src-tauri/src/batch_auth.rs @@ -222,6 +222,40 @@ impl ExcelRowAllocator { } } + /// Find the row whose UUID matches `uuid` and confirm it as Used (mark MAC + + /// timestamp and persist). If the row is already Used this is a no-op. + /// Returns the row index that was confirmed, or None if uuid was not found. + pub fn find_and_confirm_by_uuid( + &self, + uuid: &str, + mac: String, + ) -> Result, String> { + let row_idx = { + let state = self.state.lock().unwrap(); + state + .rows + .iter() + .enumerate() + .find(|(_, r)| r.uuid == uuid) + .map(|(i, _)| i) + }; + match row_idx { + None => Ok(None), + Some(idx) => { + // Only confirm rows that haven't been marked Used yet. + let already_used = { + let state = self.state.lock().unwrap(); + state.rows[idx].status == RowStatus::Used + }; + if already_used { + return Ok(Some(idx)); + } + self.confirm_row(idx, mac)?; + Ok(Some(idx)) + } + } + } + pub fn confirm_row(&self, row_idx: usize, mac: String) -> Result<(), String> { let mut state = self.state.lock().unwrap(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c2789f1..a135156 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -592,13 +592,24 @@ fn batch_auth_start( } let _ = app_clone.emit("batch-auth-progress", payload); } - Ok(tyutool_core::BatchAuthSlotResult::Skipped { mac }) => { - log::info!("[batch-auth] slot skipped port={port_clone} mac={mac} uuid={uuid} excel_row={row_idx}"); + Ok(tyutool_core::BatchAuthSlotResult::Skipped { mac, existing_uuid }) => { + log::info!("[batch-auth] slot skipped port={port_clone} mac={mac} existing_uuid={existing_uuid} new_row={row_idx}"); + // Release the newly-allocated row (we won't write a new auth code). alloc_clone.release_row(row_idx); - let _ = app_clone.emit( - "batch-auth-progress", - serde_json::json!({ "port": port_clone, "step": "skipped", "mac": mac }), - ); + // Mark the device's existing auth-code row as Used so the same + // code isn't handed out to another device. + let excel_err = alloc_clone + .find_and_confirm_by_uuid(&existing_uuid, mac.clone()) + .err(); + if let Some(ref e) = excel_err { + log::error!("[batch-auth] excel-confirm-skipped-failed port={port_clone} existing_uuid={existing_uuid} error={e}"); + } + let mut payload = + serde_json::json!({ "port": port_clone, "step": "skipped", "mac": mac }); + if let Some(e) = excel_err { + payload["excelError"] = serde_json::Value::String(e); + } + let _ = app_clone.emit("batch-auth-progress", payload); } Ok(tyutool_core::BatchAuthSlotResult::Cancelled) => { log::info!( diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index bfd0a54..2ba8bd2 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "tyutool", - "version": "3.0.11", + "version": "3.0.14", "identifier": "com.tyutool.desktop", "build": { "beforeDevCommand": "pnpm run dev", diff --git a/src/features/batch-flash-auth/auth-firmware.test.ts b/src/features/batch-flash-auth/auth-firmware.test.ts index 56d5b98..8c9e37d 100644 --- a/src/features/batch-flash-auth/auth-firmware.test.ts +++ b/src/features/batch-flash-auth/auth-firmware.test.ts @@ -1,5 +1,9 @@ import { describe, it, expect } from "vitest"; -import { filterByChip, AUTH_FIRMWARE_SOURCES } from "./auth-firmware"; +import { + filterByChip, + AUTH_FIRMWARE_SOURCES, + downloadAuthFirmware, +} from "./auth-firmware"; import type { AuthFirmwareEntry } from "./types"; const mk = (version: string, chip: string): AuthFirmwareEntry => ({ @@ -69,3 +73,16 @@ describe("AUTH_FIRMWARE_SOURCES", () => { } }); }); + +describe("downloadAuthFirmware", () => { + it("throws in web mode (isTauriRuntime=false)", async () => { + await expect( + downloadAuthFirmware({ + version: "1.1.0", + chip: "t5ai", + url: "https://example.com/fw.bin", + sha256: "abc123", + }), + ).rejects.toThrow("download requires desktop runtime"); + }); +}); diff --git a/src/stores/batch-flash-auth.test.ts b/src/stores/batch-flash-auth.test.ts index f9b2335..1dff58a 100644 --- a/src/stores/batch-flash-auth.test.ts +++ b/src/stores/batch-flash-auth.test.ts @@ -549,3 +549,387 @@ describe("useBatchFlashAuthStore firmware source", () => { expect(store.defaultFirmwareStatus).toBe("idle"); }); }); + +describe("removeSlot — additional statuses", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("cannot remove a reading_mac slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "reading_mac"; + store.removeSlot("COM3"); + expect(store.slots).toHaveLength(1); + }); + + it("cannot remove an authorizing slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "authorizing"; + store.removeSlot("COM3"); + expect(store.slots).toHaveLength(1); + }); + + it("cannot remove a failed slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "failed"; + store.removeSlot("COM3"); + expect(store.slots).toHaveLength(1); + }); +}); + +describe("slot defaults and field merging", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("initial slot has correct default fields including excelError", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + const s = store.slots[0]; + expect(s.status).toBe("idle"); + expect(s.progress).toBe(0); + expect(s.currentPhase).toBe(""); + expect(s.mac).toBeUndefined(); + expect(s.error).toBeUndefined(); + expect(s.excelError).toBeUndefined(); + }); + + it("updateSlot patch only changes named fields — mac preserved when updating phase", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].mac = "112233445566"; + store.handleAuthProgress({ port: "COM3", step: "reading_mac" }); + expect(store.slots[0].mac).toBe("112233445566"); + }); + + it("excelError: undefined in patch clears the field", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].excelError = "row not found"; + store.handleAuthProgress({ port: "COM3", step: "cancelled" }); + expect(store.slots[0].excelError).toBeUndefined(); + }); +}); + +describe("handleFlashProgress — port isolation", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("only updates the matching port", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5"]); + store.slots[0].status = "flashing"; + store.slots[1].status = "flashing"; + store.handleFlashProgress({ + port: "COM3", + event: { kind: "percent", value: 75 }, + }); + expect(store.slots[0].progress).toBe(75); + expect(store.slots[1].progress).toBe(0); + }); +}); + +describe("handleAuthProgress — excelError", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("skipped step propagates excelError to slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.batchStartTime = Date.now(); + store.handleAuthProgress({ + port: "COM3", + step: "skipped", + mac: "aabbccddeeff", + excelError: "row not found in spreadsheet", + }); + expect(store.slots[0].excelError).toBe("row not found in spreadsheet"); + }); + + it("skipped step without excelError leaves excelError undefined", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.batchStartTime = Date.now(); + store.handleAuthProgress({ + port: "COM3", + step: "skipped", + mac: "aabbccddeeff", + }); + expect(store.slots[0].excelError).toBeUndefined(); + }); + + it("done step propagates excelError when present in event", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.batchStartTime = Date.now(); + store.handleAuthProgress({ + port: "COM3", + step: "done", + mac: "aabb", + excelError: "confirm failed", + }); + expect(store.slots[0].excelError).toBe("confirm failed"); + }); +}); + +describe("retryFailed", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("clears excelError on failed slot", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "failed"; + store.slots[0].error = "some error"; + store.slots[0].excelError = "confirm_row failed"; + await store.retryFailed(); + expect(store.slots[0].excelError).toBeUndefined(); + }); + + it("clears error on failed slot", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.slots[0].status = "failed"; + store.slots[0].error = "timeout"; + await store.retryFailed(); + expect(store.slots[0].error).toBeUndefined(); + }); + + it("only resets failed slots — done and skipped are unchanged", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5", "COM7"]); + store.slots[0].status = "failed"; + store.slots[1].status = "done"; + store.slots[1].mac = "aabb"; + store.slots[2].status = "skipped"; + store.slots[2].mac = "ccdd"; + await store.retryFailed(); + expect(store.slots[0].status).toBe("idle"); + expect(store.slots[1].status).toBe("done"); + expect(store.slots[1].mac).toBe("aabb"); + expect(store.slots[2].status).toBe("skipped"); + expect(store.slots[2].mac).toBe("ccdd"); + }); + + it("is a no-op when canRetry is false (no failed slots)", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.completionBanner = { kind: "all-success", count: 1 }; + await store.retryFailed(); + expect(store.completionBanner).not.toBeNull(); + }); +}); + +describe("checkBatchCompletion — active slot gate", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("no-ops when any slot is still active", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5"]); + store.batchStartTime = Date.now(); + store.slots[0].status = "authorizing"; + store.slots[1].status = "done"; + store.checkBatchCompletion(); + expect(store.completionBanner).toBeNull(); + }); + + it("banner appears only after all active slots finish", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5"]); + store.batchStartTime = Date.now(); + store.currentBatchPorts = ["COM3", "COM5"]; + store.slots[0].status = "authorizing"; + store.slots[1].status = "done"; + store.checkBatchCompletion(); + expect(store.completionBanner).toBeNull(); + store.slots[0].status = "done"; + store.checkBatchCompletion(); + expect(store.completionBanner?.kind).toBe("all-success"); + }); +}); + +describe("resetAuthStats", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("resets only auth cumulative to zero, flash stats unchanged", () => { + const store = useBatchFlashAuthStore(); + store.cumulativeStats.auth = { total: 10, success: 8, fail: 2 }; + store.cumulativeStats.flash = { total: 5, success: 4, fail: 1 }; + store.resetAuthStats(); + expect(store.cumulativeStats.auth).toEqual({ + total: 0, + success: 0, + fail: 0, + }); + expect(store.cumulativeStats.flash).toEqual({ + total: 5, + success: 4, + fail: 1, + }); + }); + + it("subsequent auth events accumulate from zero after reset", () => { + const store = useBatchFlashAuthStore(); + store.cumulativeStats.auth = { total: 10, success: 8, fail: 2 }; + store.resetAuthStats(); + store.addPorts(["COM3"]); + store.batchStartTime = Date.now(); + store.handleAuthProgress({ port: "COM3", step: "done", mac: "aabb" }); + expect(store.cumulativeStats.auth.total).toBe(1); + expect(store.cumulativeStats.auth.success).toBe(1); + }); +}); + +describe("addBlockedPort / removeBlockedPort", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("addBlockedPort adds to blockedPorts list", () => { + const store = useBatchFlashAuthStore(); + store.addBlockedPort("/dev/ttyUSB0"); + expect(store.filterConfig.blockedPorts).toContain("/dev/ttyUSB0"); + }); + + it("addBlockedPort removes idle slot that matches the newly blocked port", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["/dev/ttyUSB0", "/dev/ttyUSB1"]); + store.addBlockedPort("/dev/ttyUSB0"); + expect(store.slots.map((s) => s.port)).not.toContain("/dev/ttyUSB0"); + expect(store.slots.map((s) => s.port)).toContain("/dev/ttyUSB1"); + }); + + it("addBlockedPort does NOT remove an active slot", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["/dev/ttyUSB0"]); + store.slots[0].status = "authorizing"; + store.addBlockedPort("/dev/ttyUSB0"); + expect(store.slots).toHaveLength(1); + }); + + it("addBlockedPort is idempotent — no duplicates in blocklist", () => { + const store = useBatchFlashAuthStore(); + store.addBlockedPort("/dev/ttyUSB0"); + store.addBlockedPort("/dev/ttyUSB0"); + expect( + store.filterConfig.blockedPorts.filter((p) => p === "/dev/ttyUSB0"), + ).toHaveLength(1); + }); + + it("removeBlockedPort removes from blockedPorts list", () => { + const store = useBatchFlashAuthStore(); + store.addBlockedPort("/dev/ttyUSB0"); + store.removeBlockedPort("/dev/ttyUSB0"); + expect(store.filterConfig.blockedPorts).not.toContain("/dev/ttyUSB0"); + }); + + it("filterActive reflects blockedPorts non-empty state", () => { + const store = useBatchFlashAuthStore(); + expect(store.filterActive).toBe(false); + store.addBlockedPort("/dev/ttyUSB0"); + expect(store.filterActive).toBe(true); + store.removeBlockedPort("/dev/ttyUSB0"); + expect(store.filterActive).toBe(false); + }); +}); + +describe("isBusy computed", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("is false when all slots are terminal (done / failed / skipped / idle)", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3", "COM5"]); + store.slots[0].status = "done"; + store.slots[1].status = "failed"; + expect(store.isBusy).toBe(false); + }); + + it("is true for each active status", () => { + for (const status of ["flashing", "reading_mac", "authorizing"] as const) { + setActivePinia(createPinia()); + const s = useBatchFlashAuthStore(); + s.addPorts(["COM3"]); + s.slots[0].status = status; + expect(s.isBusy).toBe(true); + } + }); +}); + +describe("startBatch / cancelAll / cancelPort — web mode no-ops", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("startBatch does not throw and leaves slots unchanged", async () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["COM3"]); + store.authConfig.excelPath = "/auth.xlsx"; + await store.startBatch(); + expect(store.slots[0].status).toBe("idle"); + }); + + it("cancelAll does not throw", async () => { + const store = useBatchFlashAuthStore(); + await expect(store.cancelAll()).resolves.toBeUndefined(); + }); + + it("cancelPort does not throw", async () => { + const store = useBatchFlashAuthStore(); + await expect(store.cancelPort("COM3")).resolves.toBeUndefined(); + }); +}); + +describe("loadPersistedData — web mode", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("is a no-op — returns without throwing", async () => { + const store = useBatchFlashAuthStore(); + await expect(store.loadPersistedData()).resolves.toBeUndefined(); + }); + + it("does not alter state", async () => { + const store = useBatchFlashAuthStore(); + store.chipId = "t5ai"; + store.authConfig.excelPath = "/my/path.xlsx"; + await store.loadPersistedData(); + expect(store.authConfig.excelPath).toBe("/my/path.xlsx"); + }); +}); + +describe("dismissBanner", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("clears the completion banner", () => { + const store = useBatchFlashAuthStore(); + store.completionBanner = { kind: "all-success", count: 1 }; + store.dismissBanner(); + expect(store.completionBanner).toBeNull(); + }); +}); + +describe("currentStats computed", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("counts active / done / failed / skipped correctly", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["a", "b", "c", "d", "e"]); + store.slots[0].status = "authorizing"; + store.slots[1].status = "done"; + store.slots[2].status = "failed"; + store.slots[3].status = "skipped"; + expect(store.currentStats).toEqual({ + active: 1, + done: 1, + failed: 1, + skipped: 1, + }); + }); +}); + +describe("addPorts — cross-call deduplication", () => { + beforeEach(() => setActivePinia(createPinia())); + + it("deduplicates ports across multiple addPorts calls", () => { + const store = useBatchFlashAuthStore(); + store.addPorts(["/dev/ttyUSB0", "/dev/ttyUSB1"]); + store.addPorts(["/dev/ttyUSB1", "/dev/ttyUSB2"]); + expect(store.slots).toHaveLength(3); + expect(store.slots.map((s) => s.port)).toEqual([ + "/dev/ttyUSB0", + "/dev/ttyUSB1", + "/dev/ttyUSB2", + ]); + }); +}); diff --git a/src/stores/batch-flash-auth.ts b/src/stores/batch-flash-auth.ts index f1b0459..f84866b 100644 --- a/src/stores/batch-flash-auth.ts +++ b/src/stores/batch-flash-auth.ts @@ -51,9 +51,6 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { // ── Session state ───────────────────────────────────────────────────────── const slots = ref([]); - // O(1) port→slot lookup; kept in sync by addPorts/removeSlot/addBlockedPort. - // Falls back to linear scan for test cases that assign store.slots directly. - const _slotMap = new Map(); const chipId = ref("esp32"); const baudRate = ref(115200); const authBaudRate = ref(115200); @@ -112,7 +109,7 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { // ── Slot helpers ────────────────────────────────────────────────────────── function findSlot(port: string): BatchSlotState | undefined { - return _slotMap.get(port) ?? slots.value.find((s) => s.port === port); + return slots.value.find((s) => s.port === port); } function updateSlot(port: string, patch: Partial) { @@ -132,7 +129,6 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { currentPhase: "", }; slots.value.push(entry); - _slotMap.set(port, entry); existing.add(port); } } @@ -147,7 +143,6 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { slot.status === "skipped" ) { slots.value = slots.value.filter((s) => s.port !== port); - _slotMap.delete(port); } } @@ -249,6 +244,7 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { progress: 0, currentPhase: "reading_mac", error: undefined, + excelError: undefined, }); } @@ -288,6 +284,7 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { progress: 0, currentPhase: "", error: undefined, + excelError: undefined, }); } await startBatch(); @@ -367,7 +364,12 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { scheduleSaveStats(); checkBatchCompletion(); } else if (step === "skipped") { - updateSlot(port, { status: "skipped", currentPhase: "", mac: ev.mac }); + updateSlot(port, { + status: "skipped", + currentPhase: "", + mac: ev.mac, + excelError: ev.excelError, + }); checkBatchCompletion(); } else if (step === "cancelled") { // Rust cancelled the slot (e.g. user hit cancel mid-flight). @@ -438,13 +440,6 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { if (!filterConfig.value.blockedPorts.includes(normalized)) { filterConfig.value.blockedPorts.push(normalized); } - // Remove idle slots that are now filtered; sync _slotMap accordingly - const removed = slots.value.filter( - (s) => - s.status === "idle" && - filterConfig.value.blockedPorts.includes(normalizePortName(s.port)), - ); - for (const s of removed) _slotMap.delete(s.port); slots.value = slots.value.filter( (s) => s.status !== "idle" ||