From 8a4271c9787359730d26b7822afdbdbe5f120eab Mon Sep 17 00:00:00 2001 From: YangJie Date: Fri, 26 Jun 2026 15:18:19 +0800 Subject: [PATCH 01/44] feat(batch-auth): sticky dashboard and page-level scroll Replace inner-list scroll with outer page scroll so the scrollbar spans the full content area. Dashboard is pinned sticky at top so cumulative stats remain visible while scrolling through slot rows. Introduces a new 'fullBleedScroll' AppShell layout variant: full-width like fullBleed but with overflow-y:auto on the main container instead of overflow:hidden, enabling sticky positioning to work correctly. --- src/components/AppShell.vue | 13 +++++++++++-- .../batch-flash-auth/BatchFlashAuthPage.vue | 8 +++++--- .../components/BatchFlashAuthSlotList.vue | 11 +++-------- src/router/index.ts | 4 ++-- src/vite-env.d.ts | 2 +- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/components/AppShell.vue b/src/components/AppShell.vue index 7c64732c..2245730b 100644 --- a/src/components/AppShell.vue +++ b/src/components/AppShell.vue @@ -12,6 +12,9 @@ const route = useRoute(); const { t } = useI18n(); const fullBleedMain = computed(() => route.meta.layout === "fullBleed"); +const fullBleedScrollMain = computed( + () => route.meta.layout === "fullBleedScroll", +); const hideChrome = computed(() => route.meta.chrome === "none"); const nav = computed(() => @@ -140,7 +143,9 @@ function toggleLang() { :class=" fullBleedMain ? 'flex flex-col px-3 py-2 sm:px-4 sm:py-3 md:px-5 md:py-3 max-lg:overflow-y-auto lg:overflow-hidden' - : 'overflow-y-auto px-4 py-5 sm:px-6 sm:py-6 md:px-8 md:py-8' + : fullBleedScrollMain + ? 'overflow-y-auto px-3 py-2 sm:px-4 sm:py-3 md:px-5 md:py-3' + : 'overflow-y-auto px-4 py-5 sm:px-6 sm:py-6 md:px-8 md:py-8' " role="main" tabindex="-1" @@ -148,7 +153,11 @@ function toggleLang() {
{ diff --git a/src/features/batch-flash-auth/components/BatchFlashAuthSlotList.vue b/src/features/batch-flash-auth/components/BatchFlashAuthSlotList.vue index 6063eb84..ec769217 100644 --- a/src/features/batch-flash-auth/components/BatchFlashAuthSlotList.vue +++ b/src/features/batch-flash-auth/components/BatchFlashAuthSlotList.vue @@ -26,7 +26,7 @@ function onRemove(port: string) { From 86b17c86e8e1f05d8664b20a790712664fcb426c Mon Sep 17 00:00:00 2001 From: YangJie Date: Fri, 26 Jun 2026 16:21:49 +0800 Subject: [PATCH 11/44] fix(types): add authorizeStorage to FlashJobPayload, add TODO for single-device OTP --- src/features/firmware-flash/flash-ipc-types.ts | 2 ++ src/stores/flash.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/features/firmware-flash/flash-ipc-types.ts b/src/features/firmware-flash/flash-ipc-types.ts index a75d736e..ef287ade 100644 --- a/src/features/firmware-flash/flash-ipc-types.ts +++ b/src/features/firmware-flash/flash-ipc-types.ts @@ -26,6 +26,8 @@ export interface FlashJobPayload { firmwarePath?: string | null; authorizeUuid?: string | null; authorizeKey?: string | null; + /** Storage destination for auth credentials (T5AI only). Not exposed in single-device auth UI yet; defaults to null → kv. */ + authorizeStorage?: "kv" | "otp" | null; } // Types aligned with tyutool_core::FlashEvent (snake_case JSON tag "kind") diff --git a/src/stores/flash.ts b/src/stores/flash.ts index 4c5b57c6..ecabdf76 100644 --- a/src/stores/flash.ts +++ b/src/stores/flash.ts @@ -510,6 +510,7 @@ export const useFlashStore = defineStore("flash", () => { kind === "authorize" ? authorizeUuid.value.trim() || null : null, authorizeKey: kind === "authorize" ? authorizeAuthKey.value.trim() || null : null, + // TODO: expose authorizeStorage for T5AI when single-device auth UI supports OTP }; } From 7a79ba0f2830cafad36eb8b59b0b2278af5d0f13 Mon Sep 17 00:00:00 2001 From: YangJie Date: Fri, 26 Jun 2026 16:35:53 +0800 Subject: [PATCH 12/44] feat(ui): merge conflict-policy and storage-mode into one wrapping row, trim i18n copy - BatchAuthConfig: conflict policy and T5AI storage mode now share a single flex-wrap row instead of two separate rows; wraps gracefully on narrow viewports - AppShell nav: allow label text to wrap on md+ screens to avoid truncation - i18n: remove "(Recommended)" tag from storageKv; shorten storageOtpWarning to be more concise in both en and zh-CN --- src/components/AppShell.vue | 5 +- .../components/BatchAuthConfig.vue | 99 ++++++++++--------- src/locales/en.json | 4 +- src/locales/zh-CN.json | 4 +- 4 files changed, 61 insertions(+), 51 deletions(-) diff --git a/src/components/AppShell.vue b/src/components/AppShell.vue index 2245730b..71d1f44f 100644 --- a/src/components/AppShell.vue +++ b/src/components/AppShell.vue @@ -106,7 +106,10 @@ function toggleLang() { class="size-5 shrink-0" aria-hidden="true" /> - {{ item.label }} + {{ item.label }} diff --git a/src/features/batch-flash-auth/components/BatchAuthConfig.vue b/src/features/batch-flash-auth/components/BatchAuthConfig.vue index 0fb14d27..2f41ec68 100644 --- a/src/features/batch-flash-auth/components/BatchAuthConfig.vue +++ b/src/features/batch-flash-auth/components/BatchAuthConfig.vue @@ -128,57 +128,64 @@ watch(() => store.authConfig.excelPath, validateExcel, { immediate: true });
- -
- {{ t("batchFlashAuth.config.conflictPolicy") }}: - - -
- - -
+ +
- {{ t("batchFlashAuth.config.storageMode") }}: - - + +
+ {{ t("batchFlashAuth.config.conflictPolicy") }}: + + +
+ +
+ {{ t("batchFlashAuth.config.storageMode") }}: + + +
diff --git a/src/locales/en.json b/src/locales/en.json index 8e44f12b..bd77c93d 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -548,9 +548,9 @@ "skip": "Skip (recommended)", "overwrite": "Overwrite", "storageMode": "Storage", - "storageKv": "KV (Recommended)", + "storageKv": "KV", "storageOtp": "OTP / eFuse", - "storageOtpWarning": "OTP writes are irreversible. Once written, credentials cannot be modified or erased. Verify credentials before proceeding.", + "storageOtpWarning": "Credentials will be permanently burned into eFuse and cannot be changed or erased.", "excelTotal": "Total", "excelUsed": "Used", "excelRemaining": "Remaining", diff --git a/src/locales/zh-CN.json b/src/locales/zh-CN.json index 9dd10a7b..a971a376 100644 --- a/src/locales/zh-CN.json +++ b/src/locales/zh-CN.json @@ -548,9 +548,9 @@ "skip": "跳过(推荐)", "overwrite": "覆盖", "storageMode": "写入位置", - "storageKv": "KV(推荐)", + "storageKv": "KV", "storageOtp": "OTP / eFuse", - "storageOtpWarning": "OTP 写入不可逆,一旦写入无法修改或清除,请确认授权码正确后再操作。", + "storageOtpWarning": "授权信息将永久烧入 eFuse,不可撤销,无法修改或清除。", "excelTotal": "总计", "excelUsed": "已用", "excelRemaining": "剩余", From b4ff43ace2b29dd2e94d176e567567683e0491e2 Mon Sep 17 00:00:00 2001 From: YangJie Date: Fri, 26 Jun 2026 16:57:59 +0800 Subject: [PATCH 13/44] feat(store): persist shared config (chip/baud rates) across sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add BatchSharedConfig interface and SHARED_CONFIG_KEY to workspace - loadBatchFlashAuthWorkspace now returns sharedConfig - loadPersistedData restores chip/baudRate/authBaudRate; first run falls back to chip manifest defaults (e.g. esp32→460800, t5ai→921600) - Watch [chipId, baudRate, authBaudRate] to auto-save on any change --- src/stores/batch-flash-auth-workspace.ts | 43 ++++++++++++++++++++++-- src/stores/batch-flash-auth.ts | 21 ++++++++++++ 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/stores/batch-flash-auth-workspace.ts b/src/stores/batch-flash-auth-workspace.ts index c6d54360..b585ef2d 100644 --- a/src/stores/batch-flash-auth-workspace.ts +++ b/src/stores/batch-flash-auth-workspace.ts @@ -11,10 +11,17 @@ export interface BatchFirmwareConfig { version: string; } +export interface BatchSharedConfig { + chipId: string; + baudRate: number; + authBaudRate: number; +} + const FIRMWARE_KEY = "batch-flash-auth-firmware"; const CUMULATIVE_KEY = "batch-flash-auth-cumulative"; const FILTER_KEY = "batch-flash-auth-port-filter"; const AUTH_CONFIG_KEY = "batch-flash-auth-config"; +const SHARED_CONFIG_KEY = "batch-flash-auth-shared-config"; const LEGACY_CUMULATIVE_KEY = "batch-flash-cumulative"; const LEGACY_FILTER_KEY = "batch-flash-port-filter"; const STORE_FILE = "settings.json"; @@ -24,9 +31,16 @@ export async function loadBatchFlashAuthWorkspace(): Promise<{ filter: PortFilterConfig | null; firmware: BatchFirmwareConfig | null; authConfig: BatchAuthConfigData | null; + sharedConfig: BatchSharedConfig | null; }> { if (!isTauriRuntime()) { - return { cumulative: null, filter: null, firmware: null, authConfig: null }; + return { + cumulative: null, + filter: null, + firmware: null, + authConfig: null, + sharedConfig: null, + }; } try { const { Store } = await import("@tauri-apps/plugin-store"); @@ -57,13 +71,22 @@ export async function loadBatchFlashAuthWorkspace(): Promise<{ const authConfig = (await store.get(AUTH_CONFIG_KEY)) ?? null; - return { cumulative, filter, firmware, authConfig }; + const sharedConfig = + (await store.get(SHARED_CONFIG_KEY)) ?? null; + + return { cumulative, filter, firmware, authConfig, sharedConfig }; } catch (e) { console.warn( "[batch-flash-auth] workspace load failed, using defaults:", e, ); - return { cumulative: null, filter: null, firmware: null, authConfig: null }; + return { + cumulative: null, + filter: null, + firmware: null, + authConfig: null, + sharedConfig: null, + }; } } @@ -122,3 +145,17 @@ export async function saveBatchFlashAuthConfig( console.warn("[batch-flash-auth] auth config save failed:", e); } } + +export async function saveBatchFlashAuthSharedConfig( + cfg: BatchSharedConfig, +): Promise { + if (!isTauriRuntime()) return; + try { + const { Store } = await import("@tauri-apps/plugin-store"); + const store = await Store.load(STORE_FILE); + await store.set(SHARED_CONFIG_KEY, cfg); + await store.save(); + } catch (e) { + console.warn("[batch-flash-auth] shared config save failed:", e); + } +} diff --git a/src/stores/batch-flash-auth.ts b/src/stores/batch-flash-auth.ts index 750880b9..5caf2c41 100644 --- a/src/stores/batch-flash-auth.ts +++ b/src/stores/batch-flash-auth.ts @@ -33,6 +33,7 @@ import { saveBatchFlashAuthFilterConfig, saveBatchFlashAuthFirmwareConfig, saveBatchFlashAuthConfig, + saveBatchFlashAuthSharedConfig, } from "@/stores/batch-flash-auth-workspace"; const ACTIVE_STATUSES: BatchSlotStatus[] = [ @@ -507,6 +508,7 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { filter, firmware, authConfig: savedAuthConfig, + sharedConfig, } = await loadBatchFlashAuthWorkspace(); if (cumulative) cumulativeStats.value = cumulative; if (filter) filterConfig.value = filter; @@ -516,6 +518,16 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { authStorage: (savedAuthConfig.authStorage as "kv" | "otp") ?? "kv", }; } + if (sharedConfig) { + chipId.value = sharedConfig.chipId; + baudRate.value = sharedConfig.baudRate; + authBaudRate.value = sharedConfig.authBaudRate; + } else { + // First run: apply manifest defaults for the initial chip. + const m = chipManifest(chipId.value); + baudRate.value = m.defaultBaudRate; + authBaudRate.value = m.defaultAuthBaudRate; + } if (firmware) { firmwareSource.value = firmware.source; selectedDefaultVersion.value = firmware.version; @@ -537,6 +549,14 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { await saveBatchFlashAuthConfig(authConfig.value); } + async function saveSharedConfig() { + await saveBatchFlashAuthSharedConfig({ + chipId: chipId.value, + baudRate: baudRate.value, + authBaudRate: authBaudRate.value, + }); + } + // ── Event listener lifecycle ────────────────────────────────────────────── async function ensureListener() { if (!isTauriRuntime()) return; @@ -583,6 +603,7 @@ export const useBatchFlashAuthStore = defineStore("batch-flash-auth", () => { } watch(authConfig, () => void saveAuthConfig(), { deep: true }); + watch([chipId, baudRate, authBaudRate], () => void saveSharedConfig()); return { // State From a1566a5b32684abfcb4593ae1ab827e1d2eb39e2 Mon Sep 17 00:00:00 2001 From: YangJie Date: Fri, 26 Jun 2026 17:21:03 +0800 Subject: [PATCH 14/44] feat(auth): pass storage param to auth-read for OTP; align storage-mode selector right MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - auth_read() now accepts AuthStorage; OTP sends "auth-read 1", KV sends "auth-read" (no param) to stay compatible with older firmware - auth_write already had the same pattern for the write side; read side now matches - Add two unit tests: auth_read_kv_omits_storage_param and auth_read_otp_appends_storage_param - UI: push "写入位置" group to the right via ml-auto --- crates/tyutool-core/src/authorize.rs | 83 +++++++++++++------ .../components/BatchAuthConfig.vue | 2 +- 2 files changed, 60 insertions(+), 25 deletions(-) diff --git a/crates/tyutool-core/src/authorize.rs b/crates/tyutool-core/src/authorize.rs index e2ebeb28..3636d1c9 100644 --- a/crates/tyutool-core/src/authorize.rs +++ b/crates/tyutool-core/src/authorize.rs @@ -456,9 +456,14 @@ impl AuthSession { self.read_response_timed(CMD_TIMEOUT, idle) } - /// Send `auth-read` and return `(uuid, authkey)` or `None`. - fn auth_read(&mut self) -> Option<(String, String)> { - self.send_cmd("auth-read").ok()?; + /// Send `auth-read` (or `auth-read ` for non-KV storage) and return `(uuid, authkey)` or `None`. + fn auth_read(&mut self, storage: AuthStorage) -> Option<(String, String)> { + let cmd = if storage == AuthStorage::Kv { + "auth-read".to_string() + } else { + format!("auth-read {}", storage.as_u8()) + }; + self.send_cmd(&cmd).ok()?; let lines = self.read_response(); let relevant: Vec<&str> = lines .iter() @@ -778,13 +783,14 @@ where FirmwareKind::New(_) => { // ── New firmware: 2 retries / 200ms to absorb single-frame noise ── log::info!("flash.log.auth.readDeviceAuth"); + let storage = job.authorize_storage.unwrap_or_default(); let existing_auth = { let mut auth = None; for _ in 0..2u32 { if cancel.load(Ordering::Relaxed) { return Err(FlashError::Cancelled); } - auth = sess.auth_read(); + auth = sess.auth_read(storage); if auth.is_some() { break; } @@ -839,7 +845,6 @@ where // Keep 2000ms idle: device may reboot after writing auth on some // firmware builds; a short idle would exit before the reboot banner // and leave drain_boot_output unable to clear it in time. - let storage = job.authorize_storage.unwrap_or_default(); let _lines = sess.auth_write(&uuid, &authkey, storage, Duration::from_millis(2000)); if cancel.load(Ordering::Relaxed) { @@ -862,7 +867,7 @@ where if cancel.load(Ordering::Relaxed) { return Err(FlashError::Cancelled); } - result = sess.auth_read(); + result = sess.auth_read(storage); if result.is_some() { break; } @@ -896,12 +901,13 @@ where // Optional read: skip write if device already matches log::info!("flash.log.auth.readDeviceAuth"); + let storage = job.authorize_storage.unwrap_or_default(); let mut existing_auth: Option<(String, String)> = None; for _attempt in 1..=5u32 { if cancel.load(Ordering::Relaxed) { return Err(FlashError::Cancelled); } - existing_auth = sess.auth_read(); + existing_auth = sess.auth_read(storage); if existing_auth.is_some() { break; } @@ -951,7 +957,6 @@ where } log::info!("flash.log.auth.writeStart"); - let storage = job.authorize_storage.unwrap_or_default(); let _lines = sess.auth_write(&uuid, &authkey, storage, Duration::from_millis(2000)); if cancel.load(Ordering::Relaxed) { @@ -976,7 +981,7 @@ where } log::info!("flash.log.auth.verify"); - match sess.auth_read() { + match sess.auth_read(storage) { Some((rb_uuid, rb_key)) if rb_uuid == uuid && rb_key == authkey => { log::info!("flash.log.auth.verifyOk"); Ok(()) @@ -999,7 +1004,7 @@ where } else { // ── Read-only flow ──────────────────────────────────────────────────── log::info!("flash.log.auth.readCurrent"); - match sess.auth_read() { + match sess.auth_read(AuthStorage::default()) { Some((existing_uuid, existing_key)) => { if existing_uuid == PLACEHOLDER_UUID { progress(FlashEvent::Milestone { @@ -1094,7 +1099,7 @@ where let mut auth = None; for _ in 0..sess.timing.auth_read_retries { check_cancel!(); - auth = sess.auth_read(); + auth = sess.auth_read(auth_storage); if auth.is_some() { break; } @@ -1139,7 +1144,7 @@ where let mut result = None; for _ in 0..sess.timing.auth_read_retries { check_cancel!(); - result = sess.auth_read(); + result = sess.auth_read(auth_storage); if result.is_some() { break; } @@ -1194,7 +1199,7 @@ where let old_retry_ms = sess.timing.auth_read_retry_ms * 4; for _ in 0..sess.timing.auth_read_retries + 1 { check_cancel!(); - auth = sess.auth_read(); + auth = sess.auth_read(auth_storage); if auth.is_some() { break; } @@ -1240,7 +1245,7 @@ where let mut verify_result = None; for _ in 0..sess.timing.auth_read_retries { check_cancel!(); - verify_result = sess.auth_read(); + verify_result = sess.auth_read(auth_storage); if verify_result.is_some() { break; } @@ -1363,7 +1368,7 @@ mod tests { ); let mut sess = session(mock); assert_eq!( - sess.auth_read(), + sess.auth_read(AuthStorage::Kv), Some(( "uuid12345678901234".to_string(), "keyabcdefghijklmnopqrstuvwxyz012".to_string() @@ -1381,7 +1386,7 @@ mod tests { ); let mut sess = session(mock); assert_eq!( - sess.auth_read(), + sess.auth_read(AuthStorage::Kv), Some(( "uuid12345678901234".to_string(), "keyabcdefghijklmnopqrstuvwxyz012".to_string() @@ -1395,7 +1400,7 @@ mod tests { mock.add_response("auth-read\r\n\x1b[32muuid12345678901234\x1b[0m\r\nkeyabcdefghijklmnopqrstuvwxyz012\r\n"); let mut sess = session(mock); assert_eq!( - sess.auth_read(), + sess.auth_read(AuthStorage::Kv), Some(( "uuid12345678901234".to_string(), "keyabcdefghijklmnopqrstuvwxyz012".to_string() @@ -1409,7 +1414,7 @@ mod tests { // Only one relevant line after filtering — not enough for a pair. mock.add_response("auth-read\r\nuuid12345678901234\r\ntuya>\r\n"); let mut sess = session(mock); - assert_eq!(sess.auth_read(), None); + assert_eq!(sess.auth_read(AuthStorage::Kv), None); } #[test] @@ -1417,7 +1422,7 @@ mod tests { let mut mock = MockAuthIo::new(); mock.add_response(""); let mut sess = session(mock); - assert_eq!(sess.auth_read(), None); + assert_eq!(sess.auth_read(AuthStorage::Kv), None); } #[test] @@ -1425,7 +1430,7 @@ mod tests { let mut mock = MockAuthIo::new(); mock.add_response("auth-read\r\n[04-24 10:30:00] [INFO] only logs\r\ntuya>\r\n"); let mut sess = session(mock); - assert_eq!(sess.auth_read(), None); + assert_eq!(sess.auth_read(AuthStorage::Kv), None); } #[test] @@ -1439,7 +1444,7 @@ mod tests { )); let mut sess = session(mock); assert_eq!( - sess.auth_read(), + sess.auth_read(AuthStorage::Kv), Some(( PLACEHOLDER_UUID.to_string(), "keyabcdefghijklmnopqrstuvwxyz012".to_string() @@ -1709,7 +1714,7 @@ mod tests { let firmware = sess.detect_firmware(&cancel2).unwrap(); assert_eq!(firmware, FirmwareKind::New(CliVersion(1, 0, 0))); // auth_read → None (fresh device) - let existing = sess.auth_read(); + let existing = sess.auth_read(AuthStorage::Kv); assert!(existing.is_none()); // auth_write let _lines = sess.auth_write( @@ -1721,7 +1726,7 @@ mod tests { sess.drain_boot_output(); sess.wake_shell(); // no timing arg — uses self.timing // verify - let verified = sess.auth_read(); + let verified = sess.auth_read(AuthStorage::Kv); assert_eq!( verified, Some(( @@ -1754,7 +1759,7 @@ mod tests { let cancel = AtomicBool::new(false); let firmware = sess.detect_firmware(&cancel).unwrap(); assert!(matches!(firmware, FirmwareKind::New(_))); - let existing = sess.auth_read(); + let existing = sess.auth_read(AuthStorage::Kv); assert!(existing.is_some()); let (ex_u, _ex_k) = existing.unwrap(); assert_eq!(ex_u, "existinguuid1234567"); @@ -1792,4 +1797,34 @@ mod tests { ); assert!(sess.port.sent_str().contains("auth myuuid mykey 1\r\n")); } + + #[test] + fn auth_read_kv_omits_storage_param() { + let mut mock = MockAuthIo::new(); + mock.add_response( + "auth-read\r\nuuid12345678901234\r\nkeyabcdefghijklmnopqrstuvwxyz012\r\n", + ); + let mut sess = session(mock); + let _ = sess.auth_read(AuthStorage::Kv); + assert!(sess.port.sent_str().contains("auth-read\r\n")); + assert!(!sess.port.sent_str().contains("auth-read 0\r\n")); + } + + #[test] + fn auth_read_otp_appends_storage_param() { + let mut mock = MockAuthIo::new(); + mock.add_response( + "auth-read 1\r\nuuid12345678901234\r\nkeyabcdefghijklmnopqrstuvwxyz012\r\n", + ); + let mut sess = session(mock); + let result = sess.auth_read(AuthStorage::Otp); + assert!(sess.port.sent_str().contains("auth-read 1\r\n")); + assert_eq!( + result, + Some(( + "uuid12345678901234".to_string(), + "keyabcdefghijklmnopqrstuvwxyz012".to_string() + )) + ); + } } diff --git a/src/features/batch-flash-auth/components/BatchAuthConfig.vue b/src/features/batch-flash-auth/components/BatchAuthConfig.vue index 2f41ec68..07c352a7 100644 --- a/src/features/batch-flash-auth/components/BatchAuthConfig.vue +++ b/src/features/batch-flash-auth/components/BatchAuthConfig.vue @@ -158,7 +158,7 @@ watch(() => store.authConfig.excelPath, validateExcel, { immediate: true });
{{ t("batchFlashAuth.config.storageMode") }}:
+ +
+ {{ portSlot.mac }} + {{ + portSlot.isAuthorized + ? t("batchFlashAuth.slot.authorized") + : t("batchFlashAuth.slot.notAuthorized") + }} + {{ portSlot.authUuid }} + {{ t("batchFlashAuth.slot.readError") }} +
+
+