Feature chore bug 007 #21
@@ -1836,41 +1836,51 @@
|
||||
</section>
|
||||
{:else if section === "privacy"}
|
||||
<section>
|
||||
<h3>Privacy</h3>
|
||||
<p class="muted">
|
||||
What WhispAssist is actually allowed to send off this device right now. With everything
|
||||
off (the default), nothing leaves the device at all.
|
||||
</p>
|
||||
<h3>{t("settings.privacy.title")}</h3>
|
||||
<p class="muted">{t("settings.privacy.intro")}</p>
|
||||
{#if settings.privacy}
|
||||
<div class="row">
|
||||
LLM endpoint<code>{settings.privacy.llmEndpoint || "off"}</code>
|
||||
{t("settings.privacy.llm_endpoint")}<code
|
||||
>{settings.privacy.llmEndpoint || t("settings.privacy.off")}</code
|
||||
>
|
||||
<span class="badge" class:third={!settings.privacy.llmIsLocal}>
|
||||
{settings.privacy.llmIsLocal ? "local-only" : "leaves this device"}
|
||||
{settings.privacy.llmIsLocal
|
||||
? t("settings.privacy.local_only")
|
||||
: t("settings.privacy.leaves_device")}
|
||||
</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
Sync<span class="badge" class:third={settings.privacy.syncEnabled}>
|
||||
{settings.privacy.syncEnabled ? "enabled" : "off"}
|
||||
{t("settings.privacy.sync_label")}<span
|
||||
class="badge"
|
||||
class:third={settings.privacy.syncEnabled}
|
||||
>
|
||||
{settings.privacy.syncEnabled
|
||||
? t("settings.privacy.enabled")
|
||||
: t("settings.privacy.off")}
|
||||
</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
MCP server<span class="badge" class:busy={settings.mcpStatus?.enabled}>
|
||||
{settings.mcpStatus?.enabled ? `on · ${settings.mcpStatus.exposeScope}` : "off"}
|
||||
{t("settings.privacy.mcp_label")}<span
|
||||
class="badge"
|
||||
class:busy={settings.mcpStatus?.enabled}
|
||||
>
|
||||
{settings.mcpStatus?.enabled
|
||||
? t("settings.privacy.mcp_on", { scope: settings.mcpStatus.exposeScope })
|
||||
: t("settings.privacy.off")}
|
||||
</span>
|
||||
</div>
|
||||
{#if settings.mcpStatus?.enabled}
|
||||
<p class="muted confirm">
|
||||
<Check size={14} aria-hidden="true" />
|
||||
Inbound on loopback only — it adds nothing to the egress list above. A connected agent may
|
||||
still forward what it reads to its own model provider; see the MCP server tab.
|
||||
{t("settings.privacy.mcp_loopback_note")}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<h4>Egress allowlist</h4>
|
||||
<h4>{t("settings.privacy.egress_title")}</h4>
|
||||
{#if settings.privacy.allowlistedHosts.length === 0}
|
||||
<p class="muted confirm">
|
||||
<Check size={14} aria-hidden="true" />
|
||||
No hosts are allowlisted — WA makes no content egress.
|
||||
{t("settings.privacy.no_egress")}
|
||||
</p>
|
||||
{:else}
|
||||
<ul class="hosts">
|
||||
@@ -1881,70 +1891,90 @@
|
||||
{/if}
|
||||
|
||||
{#if settings.privacy.syncTargets.length > 0}
|
||||
<h4>Sync targets</h4>
|
||||
<h4>{t("settings.privacy.sync_targets_title")}</h4>
|
||||
<ul class="targets">
|
||||
{#each settings.privacy.syncTargets as t (t.name)}
|
||||
{#each settings.privacy.syncTargets as target (target.name)}
|
||||
<li>
|
||||
<span class="name">{t.name}</span>
|
||||
<span class="badge" class:third={t.thirdParty}
|
||||
>{t.thirdParty ? "third-party" : "your server"}</span
|
||||
<span class="name">{target.name}</span>
|
||||
<span class="badge" class:third={target.thirdParty}
|
||||
>{target.thirdParty
|
||||
? t("settings.privacy.third_party")
|
||||
: t("settings.privacy.your_server")}</span
|
||||
>
|
||||
<span class="host">{target.host}</span>
|
||||
<span class="badge" class:third={!target.tls}
|
||||
>{target.tls ? t("settings.privacy.tls") : t("settings.privacy.no_tls")}</span
|
||||
>
|
||||
<span class="host">{t.host}</span>
|
||||
<span class="badge" class:third={!t.tls}>{t.tls ? "TLS" : "no TLS"}</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
|
||||
<button class="link" onclick={() => settings.loadPrivacy()}>Refresh</button>
|
||||
<button class="link" onclick={() => settings.loadPrivacy()}
|
||||
>{t("settings.privacy.refresh")}</button
|
||||
>
|
||||
{:else}
|
||||
<p class="muted">Privacy self-check unavailable.</p>
|
||||
<p class="muted">{t("settings.privacy.unavailable")}</p>
|
||||
{/if}
|
||||
|
||||
{#if vault}
|
||||
<h4>Encryption vault</h4>
|
||||
<h4>{t("settings.privacy.vault_title")}</h4>
|
||||
{#if !vault.enabled}
|
||||
<p class="muted">
|
||||
Encrypt notes, transcripts, and summaries at rest with a password. (Audio files are
|
||||
not encrypted yet.)
|
||||
</p>
|
||||
<p class="muted">{t("settings.privacy.vault_intro")}</p>
|
||||
<div class="grid">
|
||||
<label class="wide"
|
||||
>Vault password<input type="password" bind:value={vaultPw} /></label
|
||||
>{t("settings.privacy.vault_password")}<input
|
||||
type="password"
|
||||
bind:value={vaultPw}
|
||||
/></label
|
||||
>
|
||||
</div>
|
||||
<button class="primary" onclick={enableVault} disabled={vaultPw.length < 8}
|
||||
>Enable vault</button
|
||||
>{t("settings.privacy.enable_vault")}</button
|
||||
>
|
||||
<p class="muted">
|
||||
Use at least 8 characters. If you forget it, encrypted content can't be recovered.
|
||||
</p>
|
||||
<p class="muted">{t("settings.privacy.vault_pw_hint")}</p>
|
||||
{:else if !vault.unlocked}
|
||||
<p class="muted">
|
||||
Vault is <strong>locked</strong>. Unlock to read encrypted meetings.
|
||||
{t("settings.privacy.vault_locked_1")}<strong
|
||||
>{t("settings.privacy.locked_word")}</strong
|
||||
>{t("settings.privacy.vault_locked_2")}
|
||||
</p>
|
||||
<div class="grid">
|
||||
<label class="wide">Password<input type="password" bind:value={vaultPw} /></label>
|
||||
<label class="wide"
|
||||
>{t("settings.privacy.password")}<input
|
||||
type="password"
|
||||
bind:value={vaultPw}
|
||||
/></label
|
||||
>
|
||||
</div>
|
||||
<button class="primary" onclick={unlockVault} disabled={!vaultPw}>Unlock</button>
|
||||
<button class="primary" onclick={unlockVault} disabled={!vaultPw}
|
||||
>{t("settings.privacy.unlock")}</button
|
||||
>
|
||||
{:else}
|
||||
<p class="muted">
|
||||
Vault is <strong>unlocked</strong>. New notes, transcripts, and summaries are
|
||||
encrypted at rest.
|
||||
{t("settings.privacy.vault_unlocked_1")}<strong
|
||||
>{t("settings.privacy.unlocked_word")}</strong
|
||||
>{t("settings.privacy.vault_unlocked_2")}
|
||||
</p>
|
||||
<button onclick={lockVault}>Lock now</button>
|
||||
<button onclick={lockVault}>{t("settings.privacy.lock_now")}</button>
|
||||
<details>
|
||||
<summary>Change password</summary>
|
||||
<summary>{t("settings.privacy.change_password")}</summary>
|
||||
<div class="grid">
|
||||
<label class="wide"
|
||||
>Current password<input type="password" bind:value={vaultPw} /></label
|
||||
>{t("settings.privacy.current_password")}<input
|
||||
type="password"
|
||||
bind:value={vaultPw}
|
||||
/></label
|
||||
>
|
||||
<label class="wide"
|
||||
>New password<input type="password" bind:value={vaultPw2} /></label
|
||||
>{t("settings.privacy.new_password")}<input
|
||||
type="password"
|
||||
bind:value={vaultPw2}
|
||||
/></label
|
||||
>
|
||||
</div>
|
||||
<button onclick={changeVaultPassword} disabled={!vaultPw || vaultPw2.length < 8}
|
||||
>Change password</button
|
||||
>{t("settings.privacy.change_password")}</button
|
||||
>
|
||||
</details>
|
||||
{/if}
|
||||
|
||||
Reference in new issue
Block a user