feat(ui): vault lock card, audio device test meter, quick hardware stress test
This commit is contained in:
1 parent
b7e6f54d91
commit
b14728e1c8
1 file changed
+369
-24
+369
-24
@@ -9,9 +9,16 @@
|
|||||||
import { t, i18n, LOCALES } from "../i18n/index.svelte";
|
import { t, i18n, LOCALES } from "../i18n/index.svelte";
|
||||||
import ConsentNotice from "../components/ConsentNotice.svelte";
|
import ConsentNotice from "../components/ConsentNotice.svelte";
|
||||||
import HostedAiBanner from "../components/HostedAiBanner.svelte";
|
import HostedAiBanner from "../components/HostedAiBanner.svelte";
|
||||||
|
import LevelMeter from "../components/LevelMeter.svelte";
|
||||||
import { open } from "@tauri-apps/plugin-dialog";
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
import { api, errorMessage, events } from "../api";
|
import { api, errorMessage, events } from "../api";
|
||||||
import type { BackendId, SyncKind, SyncTargetConfig, SyncTargetInfo } from "../api";
|
import type {
|
||||||
|
BackendId,
|
||||||
|
StressTestResult,
|
||||||
|
SyncKind,
|
||||||
|
SyncTargetConfig,
|
||||||
|
SyncTargetInfo,
|
||||||
|
} from "../api";
|
||||||
import { trapFocus } from "../actions/trapFocus";
|
import { trapFocus } from "../actions/trapFocus";
|
||||||
import {
|
import {
|
||||||
X,
|
X,
|
||||||
@@ -23,7 +30,11 @@
|
|||||||
CalendarDays,
|
CalendarDays,
|
||||||
UploadCloud,
|
UploadCloud,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
Lock,
|
||||||
|
LockOpen,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
|
Volume2,
|
||||||
|
Zap,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
@@ -381,11 +392,98 @@
|
|||||||
let showConsent = $state(false);
|
let showConsent = $state(false);
|
||||||
let testResult = $state<{ ok: boolean; message: string } | null>(null);
|
let testResult = $state<{ ok: boolean; message: string } | null>(null);
|
||||||
|
|
||||||
|
// ---- Audio device test (live level meter) ----
|
||||||
|
let monitorKind = $state<"input" | "loopback" | null>(null);
|
||||||
|
let monitorRms = $state(0);
|
||||||
|
let monitorPeak = $state(0);
|
||||||
|
let monitorUnlisten: (() => void) | null = null;
|
||||||
|
function stopMonitor() {
|
||||||
|
monitorUnlisten?.();
|
||||||
|
monitorUnlisten = null;
|
||||||
|
monitorKind = null;
|
||||||
|
monitorRms = 0;
|
||||||
|
monitorPeak = 0;
|
||||||
|
}
|
||||||
|
async function testDevice(kind: "input" | "loopback") {
|
||||||
|
if (monitorKind) return;
|
||||||
|
monitorKind = kind;
|
||||||
|
monitorRms = 0;
|
||||||
|
monitorPeak = 0;
|
||||||
|
monitorUnlisten = await events.onDeviceLevel((p) => {
|
||||||
|
if (p.kind !== kind) return;
|
||||||
|
if (p.done) {
|
||||||
|
stopMonitor();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
monitorRms = p.rms ?? 0;
|
||||||
|
monitorPeak = p.peak ?? 0;
|
||||||
|
});
|
||||||
|
const deviceId =
|
||||||
|
kind === "input"
|
||||||
|
? (settings.settings.audio_input_device ?? null)
|
||||||
|
: (settings.settings.audio_output_device ?? null);
|
||||||
|
try {
|
||||||
|
await api.monitorAudioLevel(kind, deviceId, 6000);
|
||||||
|
} catch (e) {
|
||||||
|
testResult = { ok: false, message: errorMessage(e) };
|
||||||
|
} finally {
|
||||||
|
stopMonitor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A 440Hz beep to the default output so the user can confirm speakers work.
|
||||||
|
function playTone() {
|
||||||
|
try {
|
||||||
|
const ctx = new AudioContext();
|
||||||
|
const osc = ctx.createOscillator();
|
||||||
|
const gain = ctx.createGain();
|
||||||
|
osc.frequency.value = 440;
|
||||||
|
gain.gain.value = 0.15;
|
||||||
|
osc.connect(gain).connect(ctx.destination);
|
||||||
|
osc.start();
|
||||||
|
osc.stop(ctx.currentTime + 0.5);
|
||||||
|
osc.onended = () => ctx.close();
|
||||||
|
} catch {
|
||||||
|
/* no Web Audio available */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Quick hardware stress test ----
|
||||||
|
let stressRunning = $state(false);
|
||||||
|
let stressProgress = $state<string | null>(null);
|
||||||
|
let stressResult = $state<StressTestResult | null>(null);
|
||||||
|
let stressError = $state<string | null>(null);
|
||||||
|
async function runStressTest() {
|
||||||
|
if (stressRunning) return;
|
||||||
|
stressRunning = true;
|
||||||
|
stressError = null;
|
||||||
|
stressResult = null;
|
||||||
|
const un = await events.onStressProgress((p) => {
|
||||||
|
stressProgress = `${p.backend} · ${p.model}`;
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
stressResult = await api.stressTestHardware();
|
||||||
|
} catch (e) {
|
||||||
|
stressError = errorMessage(e);
|
||||||
|
} finally {
|
||||||
|
un();
|
||||||
|
stressProgress = null;
|
||||||
|
stressRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function applyRecommendation() {
|
||||||
|
const r = stressResult?.recommended;
|
||||||
|
if (!r) return;
|
||||||
|
await settings.setPreferredBackend(r.backend as BackendId | "auto");
|
||||||
|
await settings.patch({ whisper_model: r.model });
|
||||||
|
}
|
||||||
|
|
||||||
// ---- At-rest encryption vault (T8.8, FR-SEC-3) ----
|
// ---- At-rest encryption vault (T8.8, FR-SEC-3) ----
|
||||||
let vault = $state<{ enabled: boolean; unlocked: boolean } | null>(null);
|
let vault = $state<{ enabled: boolean; unlocked: boolean } | null>(null);
|
||||||
let vaultPw = $state("");
|
let vaultPw = $state("");
|
||||||
let vaultPw2 = $state("");
|
let vaultPw2 = $state("");
|
||||||
let vaultMsg = $state<string | null>(null);
|
let vaultMsg = $state<string | null>(null);
|
||||||
|
let vaultMsgError = $state(false);
|
||||||
|
let showVaultPw = $state(false);
|
||||||
async function loadVault() {
|
async function loadVault() {
|
||||||
try {
|
try {
|
||||||
vault = await api.vaultStatus();
|
vault = await api.vaultStatus();
|
||||||
@@ -394,42 +492,46 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onMount(loadVault);
|
onMount(loadVault);
|
||||||
|
function setVaultMsg(msg: string | null, isError = false) {
|
||||||
|
vaultMsg = msg;
|
||||||
|
vaultMsgError = isError;
|
||||||
|
}
|
||||||
async function enableVault() {
|
async function enableVault() {
|
||||||
vaultMsg = null;
|
setVaultMsg(null);
|
||||||
try {
|
try {
|
||||||
await api.enableVault(vaultPw);
|
await api.enableVault(vaultPw);
|
||||||
vaultPw = "";
|
vaultPw = "";
|
||||||
vaultMsg = "Vault enabled and unlocked.";
|
setVaultMsg("Vault enabled and unlocked.");
|
||||||
await loadVault();
|
await loadVault();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
vaultMsg = errorMessage(e);
|
setVaultMsg(errorMessage(e), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function unlockVault() {
|
async function unlockVault() {
|
||||||
vaultMsg = null;
|
setVaultMsg(null);
|
||||||
try {
|
try {
|
||||||
await api.unlockVault(vaultPw);
|
await api.unlockVault(vaultPw);
|
||||||
vaultPw = "";
|
vaultPw = "";
|
||||||
vaultMsg = "Unlocked.";
|
setVaultMsg("Unlocked.");
|
||||||
await loadVault();
|
await loadVault();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
vaultMsg = errorMessage(e);
|
setVaultMsg(errorMessage(e), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function lockVault() {
|
async function lockVault() {
|
||||||
await api.lockVault();
|
await api.lockVault();
|
||||||
vaultMsg = "Locked.";
|
setVaultMsg("Locked.");
|
||||||
await loadVault();
|
await loadVault();
|
||||||
}
|
}
|
||||||
async function changeVaultPassword() {
|
async function changeVaultPassword() {
|
||||||
vaultMsg = null;
|
setVaultMsg(null);
|
||||||
try {
|
try {
|
||||||
await api.changeVaultPassword(vaultPw, vaultPw2);
|
await api.changeVaultPassword(vaultPw, vaultPw2);
|
||||||
vaultPw = "";
|
vaultPw = "";
|
||||||
vaultPw2 = "";
|
vaultPw2 = "";
|
||||||
vaultMsg = "Password changed.";
|
setVaultMsg("Password changed.");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
vaultMsg = errorMessage(e);
|
setVaultMsg(errorMessage(e), true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,6 +922,20 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<p class="muted">{t("settings.hardware.recording_device_hint")}</p>
|
<p class="muted">{t("settings.hardware.recording_device_hint")}</p>
|
||||||
|
<div class="device-test">
|
||||||
|
<button onclick={() => testDevice("loopback")} disabled={monitorKind !== null}>
|
||||||
|
<Volume2 size={13} aria-hidden="true" />
|
||||||
|
{monitorKind === "loopback"
|
||||||
|
? t("settings.hardware.testing")
|
||||||
|
: t("settings.hardware.test_output")}
|
||||||
|
</button>
|
||||||
|
<button onclick={playTone} disabled={monitorKind !== null}>
|
||||||
|
{t("settings.hardware.play_tone")}
|
||||||
|
</button>
|
||||||
|
{#if monitorKind === "loopback"}
|
||||||
|
<LevelMeter rms={monitorRms} peak={monitorPeak} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<label
|
<label
|
||||||
>{t("settings.hardware.microphone")}
|
>{t("settings.hardware.microphone")}
|
||||||
@@ -841,6 +957,75 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<p class="muted">{t("settings.hardware.mic_hint")}</p>
|
<p class="muted">{t("settings.hardware.mic_hint")}</p>
|
||||||
|
<div class="device-test">
|
||||||
|
<button
|
||||||
|
onclick={() => testDevice("input")}
|
||||||
|
disabled={monitorKind !== null || !settings.settings.microphone_enabled}
|
||||||
|
>
|
||||||
|
<Mic size={13} aria-hidden="true" />
|
||||||
|
{monitorKind === "input"
|
||||||
|
? t("settings.hardware.testing")
|
||||||
|
: t("settings.hardware.test_mic")}
|
||||||
|
</button>
|
||||||
|
{#if monitorKind === "input"}
|
||||||
|
<LevelMeter rms={monitorRms} peak={monitorPeak} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h4>{t("settings.hardware.stress_title")}</h4>
|
||||||
|
<p class="muted">{t("settings.hardware.stress_hint")}</p>
|
||||||
|
<button onclick={runStressTest} disabled={stressRunning}>
|
||||||
|
<Zap size={13} aria-hidden="true" />
|
||||||
|
{stressRunning ? t("settings.hardware.stress_running") : t("settings.hardware.stress_run")}
|
||||||
|
</button>
|
||||||
|
{#if stressProgress}
|
||||||
|
<p class="muted">{t("settings.hardware.stress_progress", { pair: stressProgress })}</p>
|
||||||
|
{/if}
|
||||||
|
{#if stressError}<p class="muted err">{stressError}</p>{/if}
|
||||||
|
{#if stressResult}
|
||||||
|
{#if stressResult.recommended}
|
||||||
|
<div class="stress-rec">
|
||||||
|
<ShieldCheck size={14} aria-hidden="true" />
|
||||||
|
<span
|
||||||
|
>{t("settings.hardware.stress_recommend", {
|
||||||
|
backend: stressResult.recommended.backend,
|
||||||
|
model: stressResult.recommended.model,
|
||||||
|
})}</span
|
||||||
|
>
|
||||||
|
<button class="primary" onclick={applyRecommendation}
|
||||||
|
>{t("settings.hardware.stress_apply")}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="muted">{t("settings.hardware.stress_none")}</p>
|
||||||
|
{/if}
|
||||||
|
<table class="stress-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{t("settings.hardware.stress_backend")}</th>
|
||||||
|
<th>{t("settings.hardware.stress_model")}</th>
|
||||||
|
<th>{t("settings.hardware.stress_rtf")}</th>
|
||||||
|
<th>{t("settings.hardware.stress_realtime")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each stressResult.results as r (r.backend + r.model)}
|
||||||
|
<tr>
|
||||||
|
<td>{r.backend}</td>
|
||||||
|
<td>{r.model}</td>
|
||||||
|
<td class="num">{r.rtf.toFixed(2)}×</td>
|
||||||
|
<td>
|
||||||
|
{#if r.realtime}
|
||||||
|
<Check size={13} aria-hidden="true" /> {t("settings.hardware.stress_yes")}
|
||||||
|
{:else}
|
||||||
|
{t("settings.hardware.stress_no")}
|
||||||
|
{/if}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if settings.hardware.npu?.present}
|
{#if settings.hardware.npu?.present}
|
||||||
{@const npu = settings.hardware.npu}
|
{@const npu = settings.hardware.npu}
|
||||||
@@ -1934,34 +2119,81 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if vault}
|
{#if vault}
|
||||||
|
<div
|
||||||
|
class="vault-card"
|
||||||
|
class:locked={vault.enabled && !vault.unlocked}
|
||||||
|
class:unlocked={vault.enabled && vault.unlocked}
|
||||||
|
>
|
||||||
|
<div class="vault-head">
|
||||||
|
{#if !vault.enabled}
|
||||||
|
<ShieldCheck size={18} aria-hidden="true" />
|
||||||
|
{:else if !vault.unlocked}
|
||||||
|
<Lock size={18} aria-hidden="true" />
|
||||||
|
{:else}
|
||||||
|
<LockOpen size={18} aria-hidden="true" />
|
||||||
|
{/if}
|
||||||
<h4>{t("settings.privacy.vault_title")}</h4>
|
<h4>{t("settings.privacy.vault_title")}</h4>
|
||||||
|
{#if vault.enabled}
|
||||||
|
<span class="badge" class:busy={!vault.unlocked}>
|
||||||
|
{vault.unlocked
|
||||||
|
? t("settings.privacy.unlocked_word")
|
||||||
|
: t("settings.privacy.locked_word")}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if !vault.enabled}
|
{#if !vault.enabled}
|
||||||
<p class="muted">{t("settings.privacy.vault_intro")}</p>
|
<p class="muted">{t("settings.privacy.vault_intro")}</p>
|
||||||
<div class="grid">
|
<div class="pw-row">
|
||||||
<label class="wide"
|
<input
|
||||||
>{t("settings.privacy.vault_password")}<input
|
type={showVaultPw ? "text" : "password"}
|
||||||
type="password"
|
|
||||||
bind:value={vaultPw}
|
bind:value={vaultPw}
|
||||||
/></label
|
placeholder={t("settings.privacy.vault_password")}
|
||||||
|
aria-label={t("settings.privacy.vault_password")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="icon pw-toggle"
|
||||||
|
onclick={() => (showVaultPw = !showVaultPw)}
|
||||||
|
aria-label={t("settings.privacy.show_password")}
|
||||||
|
title={t("settings.privacy.show_password")}
|
||||||
>
|
>
|
||||||
|
{#if showVaultPw}<EyeOff size={14} aria-hidden="true" />{:else}<Eye
|
||||||
|
size={14}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>{/if}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="primary" onclick={enableVault} disabled={vaultPw.length < 8}
|
<button class="primary" onclick={enableVault} disabled={vaultPw.length < 8}
|
||||||
>{t("settings.privacy.enable_vault")}</button
|
>{t("settings.privacy.enable_vault")}</button
|
||||||
>
|
>
|
||||||
<p class="muted">{t("settings.privacy.vault_pw_hint")}</p>
|
<p class="muted hint">{t("settings.privacy.vault_pw_hint")}</p>
|
||||||
{:else if !vault.unlocked}
|
{:else if !vault.unlocked}
|
||||||
<p class="muted">
|
<p class="muted">
|
||||||
{t("settings.privacy.vault_locked_1")}<strong
|
{t("settings.privacy.vault_locked_1")}<strong
|
||||||
>{t("settings.privacy.locked_word")}</strong
|
>{t("settings.privacy.locked_word")}</strong
|
||||||
>{t("settings.privacy.vault_locked_2")}
|
>{t("settings.privacy.vault_locked_2")}
|
||||||
</p>
|
</p>
|
||||||
<div class="grid">
|
<div class="pw-row">
|
||||||
<label class="wide"
|
<input
|
||||||
>{t("settings.privacy.password")}<input
|
type={showVaultPw ? "text" : "password"}
|
||||||
type="password"
|
|
||||||
bind:value={vaultPw}
|
bind:value={vaultPw}
|
||||||
/></label
|
placeholder={t("settings.privacy.password")}
|
||||||
|
aria-label={t("settings.privacy.password")}
|
||||||
|
onkeydown={(e) => e.key === "Enter" && vaultPw && unlockVault()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="icon pw-toggle"
|
||||||
|
onclick={() => (showVaultPw = !showVaultPw)}
|
||||||
|
aria-label={t("settings.privacy.show_password")}
|
||||||
|
title={t("settings.privacy.show_password")}
|
||||||
>
|
>
|
||||||
|
{#if showVaultPw}<EyeOff size={14} aria-hidden="true" />{:else}<Eye
|
||||||
|
size={14}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>{/if}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="primary" onclick={unlockVault} disabled={!vaultPw}
|
<button class="primary" onclick={unlockVault} disabled={!vaultPw}
|
||||||
>{t("settings.privacy.unlock")}</button
|
>{t("settings.privacy.unlock")}</button
|
||||||
@@ -1972,7 +2204,10 @@
|
|||||||
>{t("settings.privacy.unlocked_word")}</strong
|
>{t("settings.privacy.unlocked_word")}</strong
|
||||||
>{t("settings.privacy.vault_unlocked_2")}
|
>{t("settings.privacy.vault_unlocked_2")}
|
||||||
</p>
|
</p>
|
||||||
<button onclick={lockVault}>{t("settings.privacy.lock_now")}</button>
|
<button onclick={lockVault}>
|
||||||
|
<Lock size={14} aria-hidden="true" />
|
||||||
|
{t("settings.privacy.lock_now")}
|
||||||
|
</button>
|
||||||
<details>
|
<details>
|
||||||
<summary>{t("settings.privacy.change_password")}</summary>
|
<summary>{t("settings.privacy.change_password")}</summary>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
@@ -1994,7 +2229,8 @@
|
|||||||
>
|
>
|
||||||
</details>
|
</details>
|
||||||
{/if}
|
{/if}
|
||||||
{#if vaultMsg}<p class="muted">{vaultMsg}</p>{/if}
|
{#if vaultMsg}<p class="vault-msg" class:error={vaultMsgError}>{vaultMsg}</p>{/if}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
{:else if section === "language"}
|
{:else if section === "language"}
|
||||||
@@ -2445,6 +2681,115 @@
|
|||||||
color: var(--accent, #2563eb);
|
color: var(--accent, #2563eb);
|
||||||
border-color: currentColor;
|
border-color: currentColor;
|
||||||
}
|
}
|
||||||
|
.vault-card {
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-md, 8px);
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.6rem;
|
||||||
|
}
|
||||||
|
.vault-card.locked {
|
||||||
|
border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
|
||||||
|
}
|
||||||
|
.vault-card.unlocked {
|
||||||
|
border-color: color-mix(in srgb, var(--success, #16a34a) 45%, var(--border));
|
||||||
|
}
|
||||||
|
.vault-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.vault-head h4 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.vault-head :global(svg) {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.vault-card.locked .vault-head :global(svg) {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.vault-card.unlocked .vault-head :global(svg) {
|
||||||
|
color: var(--success, #16a34a);
|
||||||
|
}
|
||||||
|
.vault-head .badge {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
.pw-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
max-width: 22rem;
|
||||||
|
}
|
||||||
|
.pw-row input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.pw-toggle {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.vault-card .hint {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.vault-msg {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--success, #16a34a);
|
||||||
|
}
|
||||||
|
.vault-msg.error {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
.device-test {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 0.35rem 0 0.6rem;
|
||||||
|
}
|
||||||
|
.device-test :global(.meter) {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 8rem;
|
||||||
|
}
|
||||||
|
.stress-rec {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0.6rem 0;
|
||||||
|
padding: 0.55rem 0.75rem;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--border));
|
||||||
|
border-radius: var(--radius-md, 8px);
|
||||||
|
background: color-mix(in srgb, var(--accent) 10%, var(--bg));
|
||||||
|
}
|
||||||
|
.stress-rec :global(svg) {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.stress-rec span {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.stress-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
.stress-table th,
|
||||||
|
.stress-table td {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.stress-table th {
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.stress-table .num {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.err {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
.npu-package {
|
.npu-package {
|
||||||
margin-top: 0.6rem;
|
margin-top: 0.6rem;
|
||||||
padding: 0.6rem 0.75rem;
|
padding: 0.6rem 0.75rem;
|
||||||
|
|||||||
Reference in new issue
Block a user