diff --git a/docs/architecture/updates.md b/docs/architecture/updates.md
index 6196d046e2..f872a7ffde 100644
--- a/docs/architecture/updates.md
+++ b/docs/architecture/updates.md
@@ -12,17 +12,17 @@ How to cut a release is in [`surfsense_local/RELEASE.md`](../../surfsense_local/
Updates are a call the app makes on its own, so they are off until the user turns them on.
-- A new install asks in onboarding's Privacy choices step, beside usage statistics, with neither answer chosen for the user ([usage statistics](usage-statistics.md#consent)). An install that finished onboarding earlier keeps the setting it had.
+- A new install asks in onboarding's first step, Privacy choices, beside usage statistics, with neither answer chosen for the user ([usage statistics](usage-statistics.md#consent)). An install that finished onboarding earlier keeps the setting it had.
- The preference is `updates.json` in Electron's user data: `automatic`, false by default; `lastCheckedAt`, which Settings › Network shows as the App updates row's last call; and `lastAutomaticDay`, the UTC day of the last automatic check.
- The automatic check runs at most once a UTC day: at launch, and from an hourly tick once the day turns while the app stays open. The day is marked only once a check reads a manifest, from either host. A check that reads none is tried again on the hourly tick, at most three times a UTC day. A day's automatic checks therefore approximate the installs open that day, with no identifier on the wire; a retry after a failed check can add a second event from one install.
-- Check now in Settings › About, the sidebar's update row and Check for Updates… in the macOS app menu share one check (`useCheckForUpdates`), which asks first while `automatic` is off, naming the host it asks. The menu shows only that prompt; the sidebar and Settings show the progress. Allow turns `automatic` on, which also enables the daily check.
+- Check now in Settings › About and Check for Updates… in the macOS app menu share one check (`useCheckForUpdates`), which asks first while `automatic` is off, naming the host it asks. Allow turns `automatic` on, which also enables the daily check. Settings shows a check's progress and result. A check from the menu, with Settings perhaps closed, reports up to date or failed in a toast; a found update shows in the title bar.
+- The title bar shows updates only while one is downloading or ready, as a small pill beside the right-panel toggle. While downloading, a ring around its arrow fills with the percentage electron-updater reports, sent to the window once per whole percent; with no size known yet, part of the ring turns. Once ready, the pill fills and restarts into the update on click. On hover or focus it widens to show its label, "Downloading 64%" or "Update", with no tooltip. It shows nothing for checking, up to date or a failed check.
- An admin policy file with `"updates": "off"` turns both off and locks the setting; Check now is disabled and says why. `"updateFeed"` names an `https` mirror instead of `updates.surfsense.com`, with no GitHub fallback ([usage statistics](usage-statistics.md#policy) says where the file lives).
- An install a package manager owns never checks: Scoop, the AUR and Nix, whose packages write `resources/install-channel`, a snap or a flatpak, known from their environment, and a deb from SurfSense's apt repository, known from `/etc/apt/sources.list.d/surfsense.list` ([contract 5](../contracts/05-desktop-telemetry.md), part C, has the order). The setting is locked as under the policy, which names the lock when both apply. Settings › About then shows only "Updates come from" that package manager, with no daily check described and no "Show exactly what's sent". The app does not construct electron-updater at all for such an install, so a repackaged deb's `package-type` cannot start its deb updater.
-- The sidebar's update row is gone while either lock holds, since a check it started would be refused.
- Scoop and the AUR must first be submitted with the first release that carries `install-channel/`, not earlier: an older release has no channel lock, so it would update itself out from under them ([packaging](../../surfsense_local/packaging/README.md)).
- Homebrew and winget run the release's own installer, so nothing marks them and the in-app updater stays available beside `brew upgrade` and `winget upgrade`. The Homebrew cask declares `auto_updates` inside `on_macos` only, so plain `brew upgrade` updates the Mac app while its bundle's version is older than the cask's, as it is whenever the in-app updater is off, and always updates the Linux AppImage, which has no bundle version to compare.
- `autoDownload` is false. The download starts from the `update-available` event, so it only ever follows a check the user enabled or clicked.
-- A downloaded update installs when the user restarts from the prompt in Settings, the sidebar or the title bar, which calls `quitAndInstall()` once the download is ready, or else the next time they quit: `autoInstallOnAppQuit`, electron-updater's default, is set to true on purpose. The update prompt says both.
+- A downloaded update installs when the user restarts from Settings or the title bar's pill, which calls `quitAndInstall()` once the download is ready, or else the next time they quit: `autoInstallOnAppQuit`, electron-updater's default, is set to true on purpose. The update prompt says both.
## What a check sends
@@ -45,7 +45,7 @@ Settings › About's "Show exactly what's sent" shows the last request to the fe
This is not an `egress.require()` destination, because electron-updater runs in the Electron main process where the backend's check cannot see it ([egress](egress.md)). A development build shows the same controls and never checks.
-An inactive electron-updater, such as its AppImage updater in an AppImage run without `$APPIMAGE`, answers a check with null and emits nothing. The check then puts back the state it found, idle unless an update was already downloaded, so Settings and the sidebar never stay on "Checking…". A check that throws keeps the `error` state electron-updater emitted first.
+An inactive electron-updater, such as its AppImage updater in an AppImage run without `$APPIMAGE`, answers a check with null and emits nothing. The check then puts back the state it found, idle unless an update was already downloaded, so Settings never stays on "Checking…". A check that throws keeps the `error` state electron-updater emitted first.
## Update size
diff --git a/docs/architecture/usage-statistics.md b/docs/architecture/usage-statistics.md
index 9acbdc6b28..95b650ce93 100644
--- a/docs/architecture/usage-statistics.md
+++ b/docs/architecture/usage-statistics.md
@@ -32,9 +32,9 @@ No identifier ties one install's reports together, but the values that never cha
## Consent
-- A new install answers in the Privacy choices step, right after the welcome in onboarding: two rows, update checks and usage statistics, each with equal Yes and No buttons, neither chosen beforehand, and Continue only once both are answered. Continue calls `completeFirstRun`, which writes both answers and `firstRunAsked`. Each row opens exactly what it sends.
-- An install that finished onboarding never sees the step, and one that answered (`firstRunAsked`) skips it. Its usage statistics stay `null`, read as off, until the user turns them on. A bridge that cannot read its state skips the step too, with both left off, rather than holding setup on an empty card.
-- Settings › About has the Usage statistics switch beside App updates, with "Show exactly what's sent": the next report as Electron main would build it now (`previewUsageReport`, not sent), the last report sent and when, and the host. Settings › Network lists the same switch as a row with its host and its last call.
+- Privacy choices is onboarding's first step, right after the welcome, counted in its progress like the model steps: two rows, update checks and usage statistics, each with equal Yes and No buttons, neither chosen beforehand, and Continue only once both are answered. Continue calls `completeFirstRun`, which writes both answers and `firstRunAsked`. Each row says in one line what it does; the info icon beside its title shows the full description with every host its answer reaches, and the link below opens exactly what it sends.
+- Onboarding always shows the step in the desktop app. An install that answered already (`firstRunAsked`), or that comes back from the first model step, sees its answers chosen. An install that finished onboarding never sees it; its usage statistics stay `null`, read as off, until the user turns them on. A bridge that cannot read its state skips the step, with both left off, rather than holding setup on an empty card.
+- Settings › About has the Usage statistics switch beside App updates. Both say what they do as onboarding does, in the same one-line summary with the full description behind an info icon, and each has "Show exactly what's sent". For usage statistics that shows the next report as Electron main would build it now (`previewUsageReport`, not sent), the last report sent and when, and the host. Settings › Network lists the same switch as a row with its host and its last call.
- Turning the switch off stops the next report; nothing already sent can be recalled, because no identifier marks which reports were this install's.
## Policy
diff --git a/surfsense_local/electron/src/main/updater.test.ts b/surfsense_local/electron/src/main/updater.test.ts
index 2ac8b1a3cd..a4e1b667c4 100644
--- a/surfsense_local/electron/src/main/updater.test.ts
+++ b/surfsense_local/electron/src/main/updater.test.ts
@@ -64,7 +64,7 @@ test("a found update is downloaded and offered, and waits for a restart or a qui
assert.deepEqual(fake.calls, ["check", "download"])
assert.deepEqual(states, [
{ status: "checking" },
- { status: "downloading", version: "1.0.1" },
+ { status: "downloading", version: "1.0.1", percent: null },
{ status: "ready", version: "1.0.1" },
])
assert.deepEqual(updates.state(), { status: "ready", version: "1.0.1" })
@@ -73,6 +73,37 @@ test("a found update is downloaded and offered, and waits for a restart or a qui
assert.deepEqual(fake.calls, ["check", "download", "install"])
})
+test("a download reports its progress in whole percents, once per step", async () => {
+ const fake = fakeAutoUpdater()
+ const states: UpdateState[] = []
+ const updates = attachUpdater(fake.updater, (state) => states.push(state), FEED)
+
+ await updates.check()
+ fake.emit("update-available", { version: "1.0.1" })
+ for (const percent of [0.4, 12.2, 12.9, 64.5, 100])
+ fake.emit("download-progress", { percent, total: 1000 })
+ // No size known yet, so no percentage to show.
+ fake.emit("download-progress", { percent: 0, total: 0 })
+
+ assert.deepEqual(
+ states.slice(1).map((state) => (state.status === "downloading" ? state.percent : state.status)),
+ [null, 0, 12, 64, 100, null]
+ )
+ assert.deepEqual(updates.state(), { status: "downloading", version: "1.0.1", percent: null })
+})
+
+test("progress after the download finished leaves it ready", async () => {
+ const fake = fakeAutoUpdater()
+ const updates = attachUpdater(fake.updater, () => undefined, FEED)
+
+ await updates.check()
+ fake.emit("update-available", { version: "1.0.1" })
+ fake.emit("update-downloaded", { version: "1.0.1" })
+ fake.emit("download-progress", { percent: 100, total: 1000 })
+
+ assert.deepEqual(updates.state(), { status: "ready", version: "1.0.1" })
+})
+
test("checks go through SurfSense's feed provider, with no headers left for downloads", () => {
const fake = fakeAutoUpdater()
fake.updater.requestHeaders = { "x-user-staging-id": "0" }
diff --git a/surfsense_local/electron/src/main/updater.ts b/surfsense_local/electron/src/main/updater.ts
index 7efefbf488..62c08cdd68 100644
--- a/surfsense_local/electron/src/main/updater.ts
+++ b/surfsense_local/electron/src/main/updater.ts
@@ -24,7 +24,8 @@ export type UpdateState =
| { status: "idle" }
| { status: "checking" }
| { status: "up-to-date" }
- | { status: "downloading"; version: string }
+ // percent: whole percents, null until the download's size is known.
+ | { status: "downloading"; version: string; percent: number | null }
| { status: "ready"; version: string }
| { status: "error"; message: string }
@@ -104,9 +105,16 @@ export function attachUpdater(
// `updater.channel`, whose setter turns on allowDowngrade.
updater.setFeedURL(surfSenseFeedOptions(feed))
updater.on("update-available", (info: { version: string }) => {
- set({ status: "downloading", version: info.version })
+ set({ status: "downloading", version: info.version, percent: null })
updater.downloadUpdate().catch(() => undefined)
})
+ // Sent once per whole percent, so the window is not redrawn many times a second.
+ updater.on("download-progress", (progress: { percent: number; total: number }) => {
+ if (current.status !== "downloading") return
+ const percent =
+ progress.total > 0 ? Math.min(100, Math.max(0, Math.floor(progress.percent))) : null
+ if (percent !== current.percent) set({ ...current, percent })
+ })
updater.on("update-downloaded", (info: { version: string }) => {
set({ status: "ready", version: info.version })
})
diff --git a/surfsense_local/frontend/src/components/ui/icons.tsx b/surfsense_local/frontend/src/components/ui/icons.tsx
index da8b2ba1cd..25b4458ab4 100644
--- a/surfsense_local/frontend/src/components/ui/icons.tsx
+++ b/surfsense_local/frontend/src/components/ui/icons.tsx
@@ -6,6 +6,7 @@ import {
AiImageEditIcon as AiImageEditIconData,
AiSearchLinesIcon as AiSearchLinesIconData,
ArrowDownIcon as ArrowDownIconData,
+ ArrowDown02Icon as ArrowDown02IconData,
ArrowExpand01Icon as ArrowExpand01IconData,
ArrowLeftIcon as ArrowLeftIconData,
ArrowRightIcon as ArrowRightIconData,
@@ -135,6 +136,7 @@ export const AiEditingIcon = createIcon(AiEditingIconData)
export const AiImageEditIcon = createIcon(AiImageEditIconData)
export const AiSearchLinesIcon = createIcon(AiSearchLinesIconData)
export const ArrowDownIcon = createIcon(ArrowDownIconData)
+export const ArrowDown02Icon = createIcon(ArrowDown02IconData)
export const ArrowExpand01Icon = createIcon(ArrowExpand01IconData)
export const ArrowLeftIcon = createIcon(ArrowLeftIconData)
export const ArrowRightIcon = createIcon(ArrowRightIconData)
diff --git a/surfsense_local/frontend/src/features/about/data-folder.tsx b/surfsense_local/frontend/src/features/about/data-folder.tsx
index 3742fc1259..220a8a8bc4 100644
--- a/surfsense_local/frontend/src/features/about/data-folder.tsx
+++ b/surfsense_local/frontend/src/features/about/data-folder.tsx
@@ -32,29 +32,29 @@ export function DataFolder() {
}
return (
-
-
- {intl.formatMessage({
- id: "about_data_folder_title",
- defaultMessage: "Your data",
- })}
-
-
- {intl.formatMessage({
- id: "about_data_folder_body",
- defaultMessage:
- "Models, workspaces and settings are kept here, not in the folder SurfSense is installed in.",
- })}
-
-
{folder}
-
-
void open()}>
+
+
+
{intl.formatMessage({
- id: "about_data_folder_open_button",
- defaultMessage: "Open folder",
+ id: "about_data_folder_title",
+ defaultMessage: "Your data",
})}
-
+
+
+ {intl.formatMessage({
+ id: "about_data_folder_body",
+ defaultMessage:
+ "Models, workspaces and settings are kept here, not in the folder SurfSense is installed in.",
+ })}
+
+
{folder}
+
void open()}>
+ {intl.formatMessage({
+ id: "about_data_folder_open_button",
+ defaultMessage: "Open folder",
+ })}
+
)
}
diff --git a/surfsense_local/frontend/src/features/dashboard/dashboard-page.tsx b/surfsense_local/frontend/src/features/dashboard/dashboard-page.tsx
index 07dc79b842..66410a7bed 100644
--- a/surfsense_local/frontend/src/features/dashboard/dashboard-page.tsx
+++ b/surfsense_local/frontend/src/features/dashboard/dashboard-page.tsx
@@ -60,7 +60,7 @@ import {
} from "@/features/office-support/office-offer"
import { StudioPanel } from "@/features/studio/studio-panel"
import { useStudio } from "@/features/studio/use-studio"
-import { UpdateButton } from "@/features/updates/update-settings"
+import { TitleBarUpdate } from "@/features/updates/title-bar-update"
import type { Workspace } from "@/features/workspaces/api"
import { useWorkspaces } from "@/features/workspaces/use-workspaces"
import { useStableCallback } from "@/hooks/use-stable-callback"
@@ -385,7 +385,7 @@ function WorkspaceDashboard({
-
+
void = vi.fn()) {
- return render(
- <>
-
-
- >
- )
+ return render( )
}
afterEach(() => {
@@ -93,130 +84,27 @@ describe("the license row", () => {
})
})
-describe("the update row", () => {
- it("checks on demand, then offers the restart once one is ready", async () => {
+describe("updates", () => {
+ it("are not in the footer: the title bar shows them", async () => {
stubLicense(license("active"))
- const bridge = stubUpdateBridge({
+ stubUpdateBridge({
automatic: true,
- state: { status: "idle" },
- })
- const user = userEvent.setup()
- renderFooter()
-
- await user.click(
- await screen.findByRole("button", { name: "Check for updates" })
- )
- expect(bridge.calls).toEqual(["check"])
-
- bridge.push({ status: "downloading", version: "1.0.1" })
- const downloading = await screen.findByRole("button", {
- name: "Downloading…",
- })
- // Clicking mid-download would only start a second check.
- expect((downloading as HTMLButtonElement).disabled).toBe(true)
-
- bridge.push({ status: "ready", version: "1.0.1" })
- await user.click(
- await screen.findByRole("button", { name: "Restart to update" })
- )
-
- expect(bridge.calls).toEqual(["check", "install"])
- })
-
- it("asks before the first check, and checks once allowed", async () => {
- stubLicense(license("active"))
- const bridge = stubUpdateBridge({
- automatic: false,
- state: { status: "idle" },
- })
- const user = userEvent.setup()
- renderFooter()
-
- await user.click(
- await screen.findByRole("button", { name: "Check for updates" })
- )
-
- // Named like any other destination, because that is what it is.
- await screen.findByRole("alertdialog", {
- name: "Allow SurfSense to check for updates?",
- })
- expect(bridge.calls).toEqual([])
-
- await user.click(screen.getByRole("button", { name: "Allow" }))
-
- // Allowing is what Settings > Network's switch does, then the call runs.
- await waitFor(() =>
- expect(bridge.calls).toEqual(["automatic:true", "check"])
- )
- })
-
- it("cancelling leaves github.com alone", async () => {
- stubLicense(license("active"))
- const bridge = stubUpdateBridge({
- automatic: false,
- state: { status: "idle" },
- })
- const user = userEvent.setup()
- renderFooter()
-
- await user.click(
- await screen.findByRole("button", { name: "Check for updates" })
- )
- await screen.findByRole("alertdialog")
- await user.click(screen.getByRole("button", { name: "Cancel" }))
-
- await waitFor(() => expect(screen.queryByRole("alertdialog")).toBeNull())
- expect(bridge.calls).toEqual([])
- })
-
- it("installs an update already downloaded without asking", async () => {
- stubLicense(license("active"))
- const bridge = stubUpdateBridge({
- automatic: false,
state: { status: "ready", version: "1.0.1" },
})
- const user = userEvent.setup()
renderFooter()
- // Installing touches no network, so it needs no permission even though
- // checking for this update would.
- await user.click(
- await screen.findByRole("button", { name: "Restart to update" })
- )
-
- expect(bridge.calls).toEqual(["install"])
- expect(screen.queryByRole("alertdialog")).toBeNull()
+ await screen.findByRole("button", { name: "License active" })
+ expect(screen.queryByRole("button", { name: /update/i })).toBeNull()
})
- it.each([
- [
- "a package manager owns updates",
- { lockedReason: "channel", channel: "snap" },
- ],
- ["a policy turned them off", { lockedReason: "policy", channel: null }],
- ] as const)("is gone while %s", async (_, lock) => {
- stubLicense(license("active"))
- const bridge = stubPrivacyBridge({ updates: { automatic: true } })
- renderFooter()
-
- // The privacy state is read, and leaves the row while nothing locks it.
- await screen.findByRole("button", { name: "Check for updates" })
- bridge.push({ updates: { locked: true, ...lock } })
-
- // A check the app refuses would make the row a button that does nothing.
- await waitFor(() =>
- expect(screen.queryByRole("button", { name: /update/i })).toBeNull()
+ it("leave no footer at all when there is no license status", () => {
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async () => new Promise(() => undefined))
)
- expect(screen.getByRole("button", { name: "License active" })).toBeTruthy()
- })
-
- it("renders no update row outside the desktop app", async () => {
- stubLicense(license("active"))
- renderFooter()
-
- // The license still reports; only the bridge-backed row is missing.
- await screen.findByRole("button", { name: "License active" })
+ stubUpdateBridge({ automatic: true, state: { status: "idle" } })
+ const { container } = renderFooter()
- expect(screen.queryByRole("button", { name: /update/i })).toBeNull()
+ expect(container.querySelector(".border-t")).toBeNull()
})
})
diff --git a/surfsense_local/frontend/src/features/dashboard/sidebar-footer.tsx b/surfsense_local/frontend/src/features/dashboard/sidebar-footer.tsx
index b35b334dc1..9f685dd64c 100644
--- a/surfsense_local/frontend/src/features/dashboard/sidebar-footer.tsx
+++ b/surfsense_local/frontend/src/features/dashboard/sidebar-footer.tsx
@@ -1,26 +1,16 @@
import type { ComponentType } from "react"
import { Button } from "@/components/ui/button"
-import { DownloadCircle02Icon, LicenseIcon } from "@/components/ui/icons"
+import { LicenseIcon } from "@/components/ui/icons"
import type { LicenseState } from "@/features/license/api"
import { useLicense } from "@/features/license/use-license"
-import { usePrivacyState } from "@/features/privacy-choices/use-privacy-state"
-import { useCheckForUpdates } from "@/features/updates/use-check-for-updates"
-import {
- updatesBridge,
- useUpdateState,
-} from "@/features/updates/use-update-state"
import { intl } from "@/i18n/intl"
-import type { UpdateState } from "@/lib/api"
import { cn } from "@/lib/utils"
// `good` and `bad` keep their colour on hover, where ghost would otherwise
// repaint them: a status light that changes colour under the pointer is no
// longer reporting anything.
const TONE = {
- quiet: "text-muted-foreground",
- offer: "text-notice hover:text-notice",
- wrong: "text-amber-600 dark:text-amber-500",
good: "text-emerald-600 hover:text-emerald-600 dark:text-emerald-500 dark:hover:text-emerald-500",
bad: "text-destructive hover:text-destructive",
} as const
@@ -28,7 +18,6 @@ const TONE = {
type Row = {
label: string
tone: keyof typeof TONE
- busy?: boolean
}
function FooterRow({
@@ -46,7 +35,6 @@ function FooterRow({
type="button"
variant="ghost"
size="xs"
- disabled={row.busy}
className={cn("w-full justify-start px-2 font-normal", TONE[row.tone])}
onClick={onClick}
>
@@ -104,68 +92,12 @@ function licenseRow(state: LicenseState): Row {
return { label: label(), tone }
}
-function updateRow(state: UpdateState): Row {
- switch (state.status) {
- case "checking":
- return {
- label: intl.formatMessage({
- id: "dashboard_footer_update_checking_status",
- defaultMessage: "Checking…",
- }),
- tone: "quiet",
- busy: true,
- }
- case "downloading":
- return {
- label: intl.formatMessage({
- id: "dashboard_footer_update_downloading_status",
- defaultMessage: "Downloading…",
- }),
- tone: "quiet",
- busy: true,
- }
- case "ready":
- return {
- label: intl.formatMessage({
- id: "dashboard_footer_update_restart_button",
- defaultMessage: "Restart to update",
- }),
- tone: "offer",
- }
- case "up-to-date":
- return {
- label: intl.formatMessage({
- id: "dashboard_footer_update_current_status",
- defaultMessage: "Up to date",
- }),
- tone: "quiet",
- }
- case "error":
- return {
- label: intl.formatMessage({
- id: "dashboard_footer_update_failed_status",
- defaultMessage: "Update check failed",
- }),
- tone: "wrong",
- }
- default:
- return {
- label: intl.formatMessage({
- id: "dashboard_footer_update_check_button",
- defaultMessage: "Check for updates",
- }),
- tone: "quiet",
- }
- }
-}
-
/**
- * The sidebar's bottom edge, holding only what is worth a glance: where the
- * license stands, and where the app stands on updates.
+ * The sidebar's bottom edge: where the license stands, at a glance. Updates
+ * show in the title bar instead.
*
- * Either row can be absent -- no update row outside the desktop app or while
- * updates are locked, no license row until its status arrives -- so the strip
- * goes with them rather than leaving a bordered gap.
+ * Absent until the license status arrives, so the strip never leaves a
+ * bordered gap.
*/
export function SidebarFooter({
onOpenLicense,
@@ -173,45 +105,15 @@ export function SidebarFooter({
onOpenLicense: () => void
}) {
const license = useLicense().data
- const updates = updatesBridge()
- const state = useUpdateState()
- const checkForUpdates = useCheckForUpdates()
- const privacy = usePrivacyState()
-
- const currentLicenseRow = license ? licenseRow(license.state) : null
- // No row while a policy or a package manager locks updates, where a click
- // would do nothing, nor until the privacy state says whether one does.
- const showUpdates =
- Boolean(updates) &&
- (privacy.state === null ||
- privacy.failed ||
- privacy.state?.updates.locked === false)
-
- if (!currentLicenseRow && !showUpdates) return null
-
- // Installing is local and needs no permission; checking goes through consent.
- const onUpdateClick = async () => {
- if (!updates) return
- if (state.status === "ready") return void updates.install()
- await checkForUpdates()
- }
+ if (!license) return null
return (
- {currentLicenseRow ? (
-
- ) : null}
- {showUpdates ? (
- void onUpdateClick()}
- />
- ) : null}
+
)
}
diff --git a/surfsense_local/frontend/src/features/onboarding/onboarding-page.test.tsx b/surfsense_local/frontend/src/features/onboarding/onboarding-page.test.tsx
index 7e21df4d00..f9f2a50556 100644
--- a/surfsense_local/frontend/src/features/onboarding/onboarding-page.test.tsx
+++ b/surfsense_local/frontend/src/features/onboarding/onboarding-page.test.tsx
@@ -1342,6 +1342,16 @@ describe("the privacy choices", () => {
await screen.findByRole("heading", { name: "Privacy choices" })
}
+ // The full description is the title's tooltip.
+ async function hoverDetails(
+ user: ReturnType,
+ label: string
+ ) {
+ await user.hover(screen.getByRole("button", { name: label }))
+ const tooltip = await screen.findByRole("tooltip")
+ return tooltip.textContent ?? ""
+ }
+
function choice(row: string, answer: "Yes" | "No") {
return within(screen.getByRole("group", { name: row })).getByRole(
"button",
@@ -1356,8 +1366,9 @@ describe("the privacy choices", () => {
render( undefined} />)
await toPrivacyStep(user)
- // Not a model step: the progress bar starts after it.
- expect(screen.queryByLabelText(/Onboarding step/)).toBeNull()
+ // The first step like any other: counted, with no way back to the welcome.
+ expect(screen.getByLabelText("Onboarding step 1 of 7")).toBeTruthy()
+ expect(screen.queryByRole("button", { name: "Back" })).toBeNull()
for (const row of ["Check for updates", "Share usage statistics"])
for (const answer of ["Yes", "No"] as const)
expect(choice(row, answer).getAttribute("aria-pressed")).toBe("false")
@@ -1387,17 +1398,32 @@ describe("the privacy choices", () => {
render( undefined} />)
await toPrivacyStep(user)
+ // One line each; the hosts are in the info icon's description.
const updates = screen.getByRole("region", { name: "Check for updates" })
- expect(updates.textContent).toContain("updates.surfsense.com")
- expect(updates.textContent).toContain("github.com")
- expect(updates.textContent).toContain("no ID")
- // The download and where it comes from, as the update prompt says.
- expect(updates.textContent).toContain("your IP address")
- expect(updates.textContent).toContain("githubusercontent.com")
- expect(updates.textContent).toContain("the next time you quit")
+ expect(updates.textContent).toContain("No ID sent.")
+ expect(updates.textContent).not.toContain("updates.surfsense.com")
const usage = screen.getByRole("region", { name: "Share usage statistics" })
- expect(usage.textContent).toContain("usage.surfsense.com")
expect(usage.textContent).toContain(
+ "Never your documents, prompts or an ID."
+ )
+
+ // Every host, and the download, as the update prompt says.
+ const updatesDetails = await hoverDetails(
+ user,
+ "What an update check sends"
+ )
+ expect(updatesDetails).toContain("updates.surfsense.com")
+ expect(updatesDetails).toContain("github.com")
+ expect(updatesDetails).toContain("your IP address")
+ expect(updatesDetails).toContain("githubusercontent.com")
+ expect(updatesDetails).toContain("the next time you quit")
+ await user.unhover(
+ screen.getByRole("button", { name: "What an update check sends" })
+ )
+ await waitFor(() => expect(screen.queryByRole("tooltip")).toBeNull())
+ const usageDetails = await hoverDetails(user, "What usage statistics send")
+ expect(usageDetails).toContain("usage.surfsense.com")
+ expect(usageDetails).toContain(
"Never documents, prompts, file names, model names, hosts, your license key or an ID."
)
})
@@ -1411,7 +1437,7 @@ describe("the privacy choices", () => {
await toPrivacyStep(user)
const usage = screen.getByRole("region", { name: "Share usage statistics" })
await user.click(
- within(usage).getByRole("button", { name: "See exactly what’s sent" })
+ within(usage).getByRole("button", { name: "Show exactly what’s sent" })
)
const dialog = await screen.findByRole("dialog", {
@@ -1433,7 +1459,7 @@ describe("the privacy choices", () => {
await toPrivacyStep(user)
const updates = screen.getByRole("region", { name: "Check for updates" })
await user.click(
- within(updates).getByRole("button", { name: "See exactly what’s sent" })
+ within(updates).getByRole("button", { name: "Show exactly what’s sent" })
)
const dialog = await screen.findByRole("dialog", {
@@ -1490,7 +1516,14 @@ describe("the privacy choices", () => {
expect(updates.textContent).toContain("Updates come from the Snap Store")
expect(updates.textContent).not.toContain("Once a day")
expect(
- within(updates).queryByRole("button", { name: "See exactly what’s sent" })
+ within(updates).queryByRole("button", {
+ name: "Show exactly what’s sent",
+ })
+ ).toBeNull()
+ expect(
+ within(updates).queryByRole("button", {
+ name: "What an update check sends",
+ })
).toBeNull()
expect(
screen.queryByRole("group", { name: "Check for updates" })
@@ -1521,16 +1554,50 @@ describe("the privacy choices", () => {
).toBeNull()
})
- it("never asks an install that already answered", async () => {
+ it("asks an install that already answered again, with its answers chosen", async () => {
vi.stubGlobal("fetch", backend())
- stubPrivacyBridge({ firstRunAsked: true })
+ stubPrivacyBridge({
+ firstRunAsked: true,
+ updates: { automatic: true },
+ usageStatistics: { enabled: false },
+ })
const user = userEvent.setup()
render( undefined} />)
- // Whether or not its answer was read before the welcome was left.
- await toChatStep(user)
+ await toPrivacyStep(user)
expect(
- screen.queryByRole("heading", { name: "Privacy choices" })
- ).toBeNull()
+ choice("Check for updates", "Yes").getAttribute("aria-pressed")
+ ).toBe("true")
+ expect(
+ choice("Share usage statistics", "No").getAttribute("aria-pressed")
+ ).toBe("true")
+ expect(
+ screen.getByRole("button", { name: "Continue" }).hasAttribute("disabled")
+ ).toBe(false)
+ })
+
+ it("goes back to the choices from the first model step, keeping them", async () => {
+ vi.stubGlobal("fetch", backend({ selections: { ...chatChosen } }))
+ stubPrivacyBridge()
+ const user = userEvent.setup()
+ render( undefined} />)
+
+ await toPrivacyStep(user)
+ await user.click(choice("Check for updates", "No"))
+ await user.click(choice("Share usage statistics", "Yes"))
+ await user.click(screen.getByRole("button", { name: "Continue" }))
+ await screen.findByRole("heading", {
+ name: "Choose a text generation model",
+ })
+ expect(screen.getByLabelText("Onboarding step 2 of 7")).toBeTruthy()
+
+ await user.click(screen.getByRole("button", { name: "Back" }))
+ await screen.findByRole("heading", { name: "Privacy choices" })
+ expect(choice("Check for updates", "No").getAttribute("aria-pressed")).toBe(
+ "true"
+ )
+ expect(
+ choice("Share usage statistics", "Yes").getAttribute("aria-pressed")
+ ).toBe("true")
})
})
diff --git a/surfsense_local/frontend/src/features/onboarding/onboarding-page.tsx b/surfsense_local/frontend/src/features/onboarding/onboarding-page.tsx
index a32213034f..b93054b7d5 100644
--- a/surfsense_local/frontend/src/features/onboarding/onboarding-page.tsx
+++ b/surfsense_local/frontend/src/features/onboarding/onboarding-page.tsx
@@ -16,14 +16,15 @@ import { PrivacyStep } from "./privacy-step/privacy-step"
import { useFinishOnboarding } from "./use-finish-onboarding"
/**
- * The steps, in order, and the ones the progress bar counts. The welcome is still
- * onboarding, and still gated by the same marker, but it is an introduction,
- * not a step to complete. Whichever step is last finishes onboarding.
+ * The model steps, in order. The privacy choices come before them as the first
+ * step the progress bar counts. The welcome is still onboarding, and still gated
+ * by the same marker, but it is an introduction, not a step to complete.
+ * Whichever step is last finishes onboarding.
*
* The embedding model comes last and is the one step that is not a slot: its
* choice is sent with the finish that fixes it, never held across steps.
*/
-const STEPS = [
+const MODEL_STEPS = [
"text_gen",
"image_gen",
"image_edit",
@@ -32,13 +33,12 @@ const STEPS = [
"embedding",
] as const satisfies readonly OnboardingStepKind[]
-type Step = (typeof STEPS)[number]
-// The privacy choices sit between the welcome and the steps, and like the
-// welcome they are not a step the progress bar counts.
-type Screen = "welcome" | "privacy" | Step
+type ModelStepKind = (typeof MODEL_STEPS)[number]
+type Step = "privacy" | ModelStepKind
+type Screen = "welcome" | Step
/** Finishing needs a chat model, so only this step cannot be skipped. */
-const REQUIRED_STEP: Step = "text_gen"
+const REQUIRED_STEP: ModelStepKind = "text_gen"
function OnboardingBrand() {
return (
@@ -60,8 +60,14 @@ function OnboardingBrand() {
)
}
-function OnboardingProgress({ screen }: { screen: Step }) {
- const step = STEPS.indexOf(screen) + 1
+function OnboardingProgress({
+ screen,
+ steps,
+}: {
+ screen: Step
+ steps: readonly Step[]
+}) {
+ const step = steps.indexOf(screen) + 1
const label = intl.formatMessage(
{
id: "onboarding_progress_aria",
@@ -69,13 +75,13 @@ function OnboardingProgress({ screen }: { screen: Step }) {
},
{
step,
- total: STEPS.length,
+ total: steps.length,
}
)
return (
label}
className="mx-auto w-full max-w-28 **:data-[slot=progress-indicator]:rounded-full **:data-[slot=progress-indicator]:duration-300 **:data-[slot=progress-indicator]:ease-out **:data-[slot=progress-track]:bg-border **:data-[slot=progress-indicator]:motion-reduce:transition-none"
@@ -172,20 +178,22 @@ function WelcomeStep({ onNext }: { onNext: () => void }) {
)
}
-/** One step in the order of `STEPS`: the last one finishes, the rest move on. */
+/** One model step in the order of `steps`: the last one finishes, the rest move on. */
function OnboardingStep({
step,
+ steps,
onGo,
onComplete,
}: {
- step: Step
+ step: ModelStepKind
+ steps: readonly Step[]
onGo: (step: Step) => void
onComplete: (selection: ModelSelection) => void
}) {
const { finish, finishing, error } = useFinishOnboarding(onComplete)
- const index = STEPS.indexOf(step)
- const previous = STEPS[index - 1]
- const next = STEPS[index + 1]
+ const index = steps.indexOf(step)
+ const previous = steps[index - 1]
+ const next = steps[index + 1]
// The last step finishes, sending its value: the embedding choice, or null.
const advance = (value: string | null) =>
next ? onGo(next) : void finish(value)
@@ -209,14 +217,16 @@ export function OnboardingPage({
}) {
const [chosen, setScreen] = useState("welcome")
const { state: privacy, failed: privacyUnreadable } = usePrivacyState()
- // Asked once, on a new install: never outside the desktop app, or once
- // answered. An unreadable state skips it, leaving both off, rather than
- // holding setup on an empty card.
- const privacyAnswered =
- privacy === null || privacy?.firstRunAsked === true || privacyUnreadable
- // Left the welcome before the answer was read, and none was needed.
+ // Always asked in the desktop app. Outside it there is nothing to save to,
+ // and an unreadable state skips it, leaving both off, rather than holding
+ // setup on an empty card.
+ const asksPrivacy = privacy !== null && !privacyUnreadable
+ const steps: readonly Step[] = asksPrivacy
+ ? ["privacy", ...MODEL_STEPS]
+ : MODEL_STEPS
+ // Left the welcome before the state was read, and it could not be.
const screen: Screen =
- chosen === "privacy" && privacyAnswered ? STEPS[0] : chosen
+ chosen === "privacy" && !asksPrivacy ? MODEL_STEPS[0] : chosen
return (
- {screen === "welcome" || screen === "privacy" ? (
+ {screen === "welcome" ? (
// The progress bar's height, kept so the hero sits exactly where it did.
) : (
-
+
)}
{screen === "welcome" ? (
-
setScreen(privacyAnswered ? STEPS[0] : "privacy")}
- />
+ setScreen(steps[0])} />
) : screen === "privacy" ? (
- // Shown once read; `screen` moves on by itself if it needs no answer.
+ // Shown once read; `screen` moves on by itself if it cannot be.
privacy ? (
- setScreen(STEPS[0])} />
+ setScreen(MODEL_STEPS[0])}
+ />
) : null
) : (
// Keyed so each step starts fresh rather than inheriting the last.
diff --git a/surfsense_local/frontend/src/features/onboarding/privacy-step/privacy-choice-row.tsx b/surfsense_local/frontend/src/features/onboarding/privacy-step/privacy-choice-row.tsx
index e8c30b86d2..c6a22a4f8c 100644
--- a/surfsense_local/frontend/src/features/onboarding/privacy-step/privacy-choice-row.tsx
+++ b/surfsense_local/frontend/src/features/onboarding/privacy-step/privacy-choice-row.tsx
@@ -1,14 +1,21 @@
-import { useId, type ReactNode } from "react"
+import { useId, type ComponentType, type ReactNode } from "react"
import { Button } from "@/components/ui/button"
+import { ButtonGroup } from "@/components/ui/button-group"
+import { CheckIcon } from "@/components/ui/icons"
+import type { ChoiceText } from "@/features/privacy-choices/choice-text"
+import { DetailsTooltip } from "@/features/privacy-choices/details-tooltip"
/**
* One question with two equal buttons and nothing chosen until the user picks.
- * A policy that holds it off shows why instead of the buttons.
+ * The summary names every host; the full description is the title's tooltip,
+ * and the exact request or report opens below. A policy that holds it off
+ * shows why instead of the buttons.
*/
export function PrivacyChoiceRow({
+ icon: Icon,
title,
- body,
+ text,
whatsSent,
value,
locked,
@@ -16,10 +23,11 @@ export function PrivacyChoiceRow({
noLabel,
onChange,
}: {
+ icon: ComponentType<{ className?: string }>
title: string
/** Left out when SurfSense would do nothing to describe. */
- body?: string
- whatsSent: ReactNode
+ text?: ChoiceText
+ whatsSent?: ReactNode
value: boolean | null
locked: string | null
yesLabel: string
@@ -32,38 +40,48 @@ export function PrivacyChoiceRow({
type="button"
variant="outline"
aria-pressed={value === choice}
- className="min-w-20 aria-pressed:border-primary aria-pressed:bg-primary/10 aria-pressed:text-foreground"
+ className="min-w-20 aria-pressed:border-primary aria-pressed:bg-primary aria-pressed:text-primary-foreground aria-pressed:hover:bg-primary/90 dark:aria-pressed:border-primary dark:aria-pressed:bg-primary dark:aria-pressed:hover:bg-primary/90"
onClick={() => onChange(choice)}
>
+ {value === choice ? : null}
{label}
)
return (
-
-
- {title}
-
- {body ? (
-
{body}
- ) : null}
- {locked ? (
-
{locked}
- ) : null}
- {whatsSent}
+
+
+
+
+
+ {title}
+
+ {text ? (
+
+ ) : null}
+
+ {text ? (
+
+ {text.summary}
+
+ ) : null}
+ {locked ? (
+
{locked}
+ ) : null}
+ {whatsSent}
+
{locked ? null : (
-
+
{option(true, yesLabel)}
{option(false, noLabel)}
-
+
)}
)
diff --git a/surfsense_local/frontend/src/features/onboarding/privacy-step/privacy-step.tsx b/surfsense_local/frontend/src/features/onboarding/privacy-step/privacy-step.tsx
index 2e1ba32034..7c87637092 100644
--- a/surfsense_local/frontend/src/features/onboarding/privacy-step/privacy-step.tsx
+++ b/surfsense_local/frontend/src/features/onboarding/privacy-step/privacy-step.tsx
@@ -10,7 +10,9 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card"
+import { ChartHistogramIcon, RefreshCwIcon } from "@/components/ui/icons"
import { ScrollFade } from "@/components/ui/scroll-fade"
+import { Separator } from "@/components/ui/separator"
import { Spinner } from "@/components/ui/spinner"
import {
updatesLockedText,
@@ -21,38 +23,18 @@ import {
privacyStateQueryKey,
} from "@/features/privacy-choices/use-privacy-state"
import { WhatsSentDialog } from "@/features/privacy-choices/whats-sent-dialog"
+import { updatesText, usageText } from "@/features/privacy-choices/choice-text"
import { updatePrefsQueryKey } from "@/features/updates/use-update-state"
import { intl } from "@/i18n/intl"
import type { PrivacyState } from "@/lib/api"
import { PrivacyChoiceRow } from "./privacy-choice-row"
-function updatesBody(state: PrivacyState) {
- const { host, fallbackHost } = state.updates
- return fallbackHost
- ? intl.formatMessage(
- {
- id: "onboarding_privacy_updates_body",
- defaultMessage:
- "Once a day, SurfSense asks {host} whether a newer version exists, sending your IP address, the app’s name and version, your operating system and its architecture, and no ID. If {host} cannot answer, it asks github.com with only your IP address and the app’s name. A newer version downloads by itself from github.com and githubusercontent.com, which also see your IP address, and installs when you restart or the next time you quit.",
- },
- { host }
- )
- : intl.formatMessage(
- {
- id: "onboarding_privacy_updates_mirror_body",
- defaultMessage:
- "Once a day, SurfSense asks {host}, the update server your organization set, whether a newer version exists, sending your IP address, the app’s name and version, your operating system and its architecture, and no ID. A newer version downloads by itself from {host} and installs when you restart or the next time you quit.",
- },
- { host }
- )
-}
-
/**
- * The first-run question about update checks and usage statistics: two
- * separate answers, neither chosen for the user, both needed to go on. Only a
- * new install sees it; a policy or package manager that holds one off answers
- * it already.
+ * Onboarding's first step, about update checks and usage statistics: two
+ * separate answers, both needed to go on. A new install has neither chosen for
+ * it; one that answered before sees its answers. A policy or package manager
+ * that holds one off answers it already.
*/
export function PrivacyStep({
state,
@@ -67,9 +49,15 @@ export function PrivacyStep({
const channelLocked = state.updates.lockedReason === "channel"
const usageLocked = usageLockedText(state)
const [updates, setUpdates] = useState
(
- updatesLocked ? false : null
+ updatesLocked ? false : state.firstRunAsked ? state.updates.automatic : null
+ )
+ const [usage, setUsage] = useState(
+ usageLocked
+ ? false
+ : state.firstRunAsked
+ ? (state.usageStatistics.enabled ?? null)
+ : null
)
- const [usage, setUsage] = useState(usageLocked ? false : null)
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
const answered = updates !== null && usage !== null
@@ -121,13 +109,26 @@ export function PrivacyStep({
className="min-h-0 flex-1"
viewportClassName="px-(--card-spacing) pb-1"
>
-
+
+ )
+ }
locked={updatesLocked}
value={updates}
onChange={setUpdates}
@@ -139,32 +140,25 @@ export function PrivacyStep({
id: "onboarding_privacy_updates_no_button",
defaultMessage: "No",
})}
- whatsSent={
- channelLocked ? null : (
-
- )
- }
/>
+
+ }
locked={usageLocked}
value={usage}
onChange={setUsage}
@@ -176,16 +170,6 @@ export function PrivacyStep({
id: "onboarding_privacy_usage_no_button",
defaultMessage: "No",
})}
- whatsSent={
-
- }
/>
diff --git a/surfsense_local/frontend/src/features/privacy-choices/choice-text.ts b/surfsense_local/frontend/src/features/privacy-choices/choice-text.ts
new file mode 100644
index 0000000000..5dae89deb5
--- /dev/null
+++ b/surfsense_local/frontend/src/features/privacy-choices/choice-text.ts
@@ -0,0 +1,71 @@
+import { intl } from "@/i18n/intl"
+import type { PrivacyState } from "@/lib/api"
+
+/**
+ * What onboarding and Settings say about each choice: a one-line summary, and
+ * the full description, with every host it reaches, behind the info icon.
+ */
+export type ChoiceText = { summary: string; details: string; label: string }
+
+export function updatesText(state: PrivacyState): ChoiceText {
+ const { host, fallbackHost } = state.updates
+ const label = intl.formatMessage({
+ id: "privacy_choices_updates_details_aria",
+ defaultMessage: "What an update check sends",
+ })
+ if (!fallbackHost)
+ return {
+ label,
+ summary: intl.formatMessage({
+ id: "privacy_choices_updates_mirror_summary_body",
+ defaultMessage:
+ "Checks your organization’s update server once a day. No ID sent.",
+ }),
+ details: intl.formatMessage(
+ {
+ id: "privacy_choices_updates_mirror_details_body",
+ defaultMessage:
+ "Once a day, SurfSense asks {host}, the update server your organization set, whether a newer version exists, sending your IP address, the app’s name and version, your operating system and its architecture, and no ID. A newer version downloads by itself from {host} and installs when you restart or the next time you quit.",
+ },
+ { host }
+ ),
+ }
+ return {
+ label,
+ summary: intl.formatMessage({
+ id: "privacy_choices_updates_summary_body",
+ defaultMessage: "Checks for a newer version once a day. No ID sent.",
+ }),
+ details: intl.formatMessage(
+ {
+ id: "privacy_choices_updates_details_body",
+ defaultMessage:
+ "Once a day, SurfSense asks {host} whether a newer version exists, sending your IP address, the app’s name and version, your operating system and its architecture, and no ID. If {host} cannot answer, it asks github.com with only your IP address and the app’s name. A newer version downloads by itself from github.com and githubusercontent.com, which also see your IP address, and installs when you restart or the next time you quit.",
+ },
+ { host }
+ ),
+ }
+}
+
+export function usageText(state: PrivacyState): ChoiceText {
+ const { host } = state.usageStatistics
+ return {
+ label: intl.formatMessage({
+ id: "privacy_choices_usage_details_aria",
+ defaultMessage: "What usage statistics send",
+ }),
+ summary: intl.formatMessage({
+ id: "privacy_choices_usage_summary_body",
+ defaultMessage:
+ "A daily count of how SurfSense is used, in ranges. Never your documents, prompts or an ID.",
+ }),
+ details: intl.formatMessage(
+ {
+ id: "privacy_choices_usage_details_body",
+ defaultMessage:
+ "Once a day, SurfSense tells {host} this install was used, with the app’s version, your system, the app’s language and the week it was installed. Once a week it adds counts in ranges: chats, agent turns, sources, Studio outputs, plugin calls, whether a local or remote model answered, and how far setup got. Its first report adds this computer’s memory and graphics card, in ranges. Never documents, prompts, file names, model names, hosts, your license key or an ID.",
+ },
+ { host }
+ ),
+ }
+}
diff --git a/surfsense_local/frontend/src/features/privacy-choices/details-tooltip.tsx b/surfsense_local/frontend/src/features/privacy-choices/details-tooltip.tsx
new file mode 100644
index 0000000000..f3c38ce499
--- /dev/null
+++ b/surfsense_local/frontend/src/features/privacy-choices/details-tooltip.tsx
@@ -0,0 +1,36 @@
+import { Button } from "@/components/ui/button"
+import { InformationCircleIcon } from "@/components/ui/icons"
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip"
+
+/** The info icon beside a choice's title, holding its full description. */
+export function DetailsTooltip({
+ label,
+ details,
+}: {
+ label: string
+ details: string
+}) {
+ return (
+
+
+ }
+ >
+
+
+
+ {details}
+
+
+ )
+}
diff --git a/surfsense_local/frontend/src/features/privacy-choices/stub-privacy-bridge.ts b/surfsense_local/frontend/src/features/privacy-choices/stub-privacy-bridge.ts
index da354f65c5..5aee3234cf 100644
--- a/surfsense_local/frontend/src/features/privacy-choices/stub-privacy-bridge.ts
+++ b/surfsense_local/frontend/src/features/privacy-choices/stub-privacy-bridge.ts
@@ -77,7 +77,15 @@ export function stubPrivacyBridge(
calls.push(
`first-run:updates=${choices.updates},usage=${choices.usageStatistics}`
)
- push({ ...state, firstRunAsked: true })
+ // Main stores both answers with the flag, as Settings would.
+ push({
+ updates: { ...state.updates, automatic: choices.updates },
+ usageStatistics: {
+ ...state.usageStatistics,
+ enabled: choices.usageStatistics,
+ },
+ firstRunAsked: true,
+ })
},
onStateChanged: (listener) => {
listeners.add(listener)
diff --git a/surfsense_local/frontend/src/features/privacy-choices/usage-statistics-settings.test.tsx b/surfsense_local/frontend/src/features/privacy-choices/usage-statistics-settings.test.tsx
index 070ffe1365..fca79a77c7 100644
--- a/surfsense_local/frontend/src/features/privacy-choices/usage-statistics-settings.test.tsx
+++ b/surfsense_local/frontend/src/features/privacy-choices/usage-statistics-settings.test.tsx
@@ -115,13 +115,59 @@ describe("App updates in Settings › About", () => {
stubPrivacyBridge({
updates: { host: "updates.example.org", fallbackHost: null },
})
+ const user = userEvent.setup()
render(
)
expect(
await screen.findByText(
- "Free updates from updates.example.org, the update server your organization set. Once you allow update checks, SurfSense asks it once a day."
+ "Checks your organization’s update server once a day. No ID sent."
+ )
+ ).toBeTruthy()
+ await user.hover(
+ screen.getByRole("button", { name: "What an update check sends" })
+ )
+ const details = (await screen.findByRole("tooltip")).textContent
+ expect(details).toContain("updates.example.org")
+ expect(details).not.toContain("github.com")
+ })
+
+ it("says each in one line, with the full description behind the info icon", async () => {
+ stubPrivacyBridge()
+ const user = userEvent.setup()
+ render(
+ <>
+
+
+ >
+ )
+
+ expect(
+ await screen.findByText(
+ "Checks for a newer version once a day. No ID sent."
)
).toBeTruthy()
+ expect(
+ screen.getByText(
+ "A daily count of how SurfSense is used, in ranges. Never your documents, prompts or an ID."
+ )
+ ).toBeTruthy()
+
+ await user.hover(
+ screen.getByRole("button", { name: "What an update check sends" })
+ )
+ expect((await screen.findByRole("tooltip")).textContent).toContain(
+ "githubusercontent.com"
+ )
+ await user.unhover(
+ screen.getByRole("button", { name: "What an update check sends" })
+ )
+ await waitFor(() => expect(screen.queryByRole("tooltip")).toBeNull())
+ await user.hover(
+ screen.getByRole("button", { name: "What usage statistics send" })
+ )
+ expect((await screen.findByRole("tooltip")).textContent).toContain(
+ "Never documents, prompts, file names, model names, hosts, your license key or an ID."
+ )
})
it("holds Check now when a policy turned update checks off", async () => {
diff --git a/surfsense_local/frontend/src/features/privacy-choices/usage-statistics-settings.tsx b/surfsense_local/frontend/src/features/privacy-choices/usage-statistics-settings.tsx
index e8dd42e43a..d33cfefa24 100644
--- a/surfsense_local/frontend/src/features/privacy-choices/usage-statistics-settings.tsx
+++ b/surfsense_local/frontend/src/features/privacy-choices/usage-statistics-settings.tsx
@@ -1,6 +1,8 @@
import { Switch } from "@/components/ui/switch"
import { intl } from "@/i18n/intl"
+import { usageText } from "./choice-text"
+import { DetailsTooltip } from "./details-tooltip"
import { usageLockedText } from "./locked-text"
import { usePrivacyState } from "./use-privacy-state"
import { WhatsSentDialog } from "./whats-sent-dialog"
@@ -11,6 +13,7 @@ export function UsageStatisticsSettings() {
if (!state) return null
const usage = state.usageStatistics
+ const text = usageText(state)
const locked = usageLockedText(state)
const title = intl.formatMessage({
id: "privacy_choices_usage_settings_title",
@@ -19,16 +22,12 @@ export function UsageStatisticsSettings() {
return (
-
{title}
+
+
{title}
+
+
- {intl.formatMessage(
- {
- id: "privacy_choices_usage_settings_body",
- defaultMessage:
- "Once a day, tell {host} this install was used, and once a week add counts in ranges, such as how many chats and sources. Never documents, prompts, file names, model names, hosts, your license key or an ID.",
- },
- { host: usage.host }
- )}
+ {text.summary}
{locked ? (
{locked}
diff --git a/surfsense_local/frontend/src/features/updates/download-ring.tsx b/surfsense_local/frontend/src/features/updates/download-ring.tsx
new file mode 100644
index 0000000000..aa6cb7b0fc
--- /dev/null
+++ b/surfsense_local/frontend/src/features/updates/download-ring.tsx
@@ -0,0 +1,46 @@
+import { cn } from "@/lib/utils"
+
+/**
+ * A ring that fills clockwise from the top as a download runs. With no
+ * percentage yet, a quarter of it turns instead.
+ */
+export function DownloadRing({
+ percent,
+ className,
+}: {
+ percent: number | null
+ className?: string
+}) {
+ return (
+
+
+
+
+ )
+}
diff --git a/surfsense_local/frontend/src/features/updates/menu-update-check.test.tsx b/surfsense_local/frontend/src/features/updates/menu-update-check.test.tsx
index 1d410962e8..fdbde1d68e 100644
--- a/surfsense_local/frontend/src/features/updates/menu-update-check.test.tsx
+++ b/surfsense_local/frontend/src/features/updates/menu-update-check.test.tsx
@@ -1,15 +1,21 @@
-import { afterEach, describe, expect, it } from "vitest"
+import { afterEach, describe, expect, it, vi } from "vitest"
import { act, cleanup, screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { EgressPrompt } from "@/features/egress/egress-prompt"
import { render } from "@/test-utils"
+import { toast } from "sonner"
import { MenuUpdateCheck } from "./menu-update-check"
import { stubUpdateBridge } from "./stub-bridge"
+vi.mock("sonner", () => ({
+ toast: { success: vi.fn(), error: vi.fn() },
+}))
+
afterEach(() => {
cleanup()
+ vi.clearAllMocks()
delete window.surfsense
})
@@ -57,4 +63,71 @@ describe("Check for Updates… in the app menu", () => {
expect(bridge.calls).toEqual(["automatic:true", "check"])
)
})
+
+ it("says when there is nothing newer, since nothing else would", async () => {
+ const bridge = stubUpdateBridge({
+ automatic: true,
+ // A result from an earlier check must not answer this one.
+ state: { status: "up-to-date" },
+ })
+ renderMenuCheck()
+
+ await act(async () => bridge.checkFromMenu())
+ expect(toast.success).not.toHaveBeenCalled()
+ act(() => bridge.push({ status: "checking" }))
+ act(() => bridge.push({ status: "up-to-date" }))
+
+ expect(toast.success).toHaveBeenCalledWith("SurfSense is up to date")
+ })
+
+ it("says when the check failed", async () => {
+ const bridge = stubUpdateBridge({
+ automatic: true,
+ state: { status: "idle" },
+ })
+ renderMenuCheck()
+
+ await act(async () => bridge.checkFromMenu())
+ act(() => bridge.push({ status: "checking" }))
+ act(() =>
+ bridge.push({
+ status: "error",
+ message: "net::ERR_INTERNET_DISCONNECTED",
+ })
+ )
+
+ expect(toast.error).toHaveBeenCalledWith("Couldn’t check for updates", {
+ description: "net::ERR_INTERNET_DISCONNECTED",
+ })
+ })
+
+ it("leaves a found update to the title bar", async () => {
+ const bridge = stubUpdateBridge({
+ automatic: true,
+ state: { status: "idle" },
+ })
+ renderMenuCheck()
+
+ await act(async () => bridge.checkFromMenu())
+ act(() => bridge.push({ status: "checking" }))
+ act(() =>
+ bridge.push({ status: "downloading", version: "1.0.1", percent: null })
+ )
+ act(() => bridge.push({ status: "up-to-date" }))
+
+ expect(toast.success).not.toHaveBeenCalled()
+ })
+
+ it("stays quiet for a check it did not start", () => {
+ const bridge = stubUpdateBridge({
+ automatic: true,
+ state: { status: "idle" },
+ })
+ renderMenuCheck()
+
+ act(() => bridge.push({ status: "checking" }))
+ act(() => bridge.push({ status: "up-to-date" }))
+
+ expect(toast.success).not.toHaveBeenCalled()
+ })
})
diff --git a/surfsense_local/frontend/src/features/updates/menu-update-check.tsx b/surfsense_local/frontend/src/features/updates/menu-update-check.tsx
index 43ffcee4e2..02f84a790b 100644
--- a/surfsense_local/frontend/src/features/updates/menu-update-check.tsx
+++ b/surfsense_local/frontend/src/features/updates/menu-update-check.tsx
@@ -1,21 +1,62 @@
import { useEffect, useRef } from "react"
+import { toast } from "sonner"
+
+import { intl } from "@/i18n/intl"
import { useCheckForUpdates } from "./use-check-for-updates"
-import { updatesBridge } from "./use-update-state"
+import { updatesBridge, useUpdateState } from "./use-update-state"
-// The app menu's Check for Updates…. Mounted once at the app root; the
-// sidebar and Settings › About show the result from the updater's own state.
+/**
+ * The app menu's Check for Updates…. Mounted once at the app root. Settings
+ * may be closed, so its own check's result comes back as a toast: up to date,
+ * or failed. A found update shows in the title bar instead.
+ */
export function MenuUpdateCheck() {
const check = useCheckForUpdates()
+ const state = useUpdateState()
// Subscribed once; the latest check carries the latest consent.
const checkRef = useRef(check)
useEffect(() => {
checkRef.current = check
})
+ // A result counts only once this check is seen running, so an earlier
+ // check's "up to date" never answers it.
+ const phase = useRef<"none" | "requested" | "running">("none")
useEffect(
- () => updatesBridge()?.onCheckRequested(() => void checkRef.current()),
+ () =>
+ updatesBridge()?.onCheckRequested(() => {
+ phase.current = "requested"
+ void checkRef.current().then((started) => {
+ if (!started && phase.current === "requested") phase.current = "none"
+ })
+ }),
[]
)
+
+ useEffect(() => {
+ if (state.status === "checking") {
+ if (phase.current === "requested") phase.current = "running"
+ return
+ }
+ if (phase.current !== "running") return
+ phase.current = "none"
+ if (state.status === "up-to-date")
+ toast.success(
+ intl.formatMessage({
+ id: "updates_menu_up_to_date_toast",
+ defaultMessage: "SurfSense is up to date",
+ })
+ )
+ else if (state.status === "error")
+ toast.error(
+ intl.formatMessage({
+ id: "updates_menu_check_failed_toast",
+ defaultMessage: "Couldn’t check for updates",
+ }),
+ { description: state.message }
+ )
+ }, [state])
+
return null
}
diff --git a/surfsense_local/frontend/src/features/updates/title-bar-update.test.tsx b/surfsense_local/frontend/src/features/updates/title-bar-update.test.tsx
new file mode 100644
index 0000000000..1a1e1fb3a6
--- /dev/null
+++ b/surfsense_local/frontend/src/features/updates/title-bar-update.test.tsx
@@ -0,0 +1,85 @@
+import { afterEach, describe, expect, it } from "vitest"
+import { cleanup, screen } from "@testing-library/react"
+import userEvent from "@testing-library/user-event"
+
+import { render } from "@/test-utils"
+
+import { stubUpdateBridge as stubBridge } from "./stub-bridge"
+import { TitleBarUpdate } from "./title-bar-update"
+
+afterEach(() => {
+ cleanup()
+ delete window.surfsense
+})
+
+describe("the title bar's update pill", () => {
+ it.each([
+ { status: "idle" },
+ { status: "checking" },
+ { status: "up-to-date" },
+ { status: "error", message: "net::ERR_INTERNET_DISCONNECTED" },
+ ] as const)(
+ "keeps no space while there is nothing to install ($status)",
+ (state) => {
+ stubBridge({ automatic: true, state })
+ const { container } = render(
)
+
+ expect(container.childElementCount).toBe(0)
+ }
+ )
+
+ it("shows a download's progress, and how far it got in its label", async () => {
+ const bridge = stubBridge({ automatic: true, state: { status: "idle" } })
+ render(
)
+ bridge.push({ status: "downloading", version: "1.0.1", percent: 64 })
+
+ const progress = await screen.findByRole("progressbar", {
+ name: "Downloading SurfSense 1.0.1",
+ })
+ expect(progress.getAttribute("aria-valuenow")).toBe("64")
+ expect(progress.textContent).toContain("Downloading 64%")
+ // A download runs by itself; there is nothing to click.
+ expect(screen.queryByRole("button")).toBeNull()
+ })
+
+ it("says only that it is downloading until the size is known", async () => {
+ const bridge = stubBridge({ automatic: true, state: { status: "idle" } })
+ render(
)
+ bridge.push({ status: "downloading", version: "1.0.1", percent: null })
+
+ const progress = await screen.findByRole("progressbar", {
+ name: "Downloading SurfSense 1.0.1",
+ })
+ expect(progress.hasAttribute("aria-valuenow")).toBe(false)
+ expect(progress.textContent).toContain("Downloading…")
+ })
+
+ it("restarts into a downloaded update, once", async () => {
+ const bridge = stubBridge({ automatic: true, state: { status: "idle" } })
+ const user = userEvent.setup()
+ render(
)
+ bridge.push({ status: "ready", version: "1.0.1" })
+
+ const button = await screen.findByRole("button", {
+ name: "Restart to install 1.0.1",
+ })
+ expect(button.textContent).toContain("Update")
+ await user.click(button)
+
+ expect(bridge.calls).toEqual(["install"])
+ expect(button.hasAttribute("disabled")).toBe(true)
+ })
+
+ it("says what it does in its own label, with no tooltip", async () => {
+ const bridge = stubBridge({ automatic: true, state: { status: "idle" } })
+ const user = userEvent.setup()
+ render(
)
+ bridge.push({ status: "ready", version: "1.0.1" })
+
+ await user.hover(
+ await screen.findByRole("button", { name: "Restart to install 1.0.1" })
+ )
+
+ expect(screen.queryByRole("tooltip")).toBeNull()
+ })
+})
diff --git a/surfsense_local/frontend/src/features/updates/title-bar-update.tsx b/surfsense_local/frontend/src/features/updates/title-bar-update.tsx
new file mode 100644
index 0000000000..400ce56b66
--- /dev/null
+++ b/surfsense_local/frontend/src/features/updates/title-bar-update.tsx
@@ -0,0 +1,143 @@
+import { useEffect, useRef, useState, type ReactNode } from "react"
+
+import { Button } from "@/components/ui/button"
+import { ArrowDown02Icon } from "@/components/ui/icons"
+import { Spinner } from "@/components/ui/spinner"
+import { intl } from "@/i18n/intl"
+import { cn } from "@/lib/utils"
+
+import { DownloadRing } from "./download-ring"
+import { updatesBridge, useUpdateState } from "./use-update-state"
+
+// A 20px circle that widens leftwards to show its label on hover or focus.
+// interpolate-size lets the width animate to fit the label, whatever its length.
+const PILL =
+ "group pointer-events-auto flex select-none h-5 w-5 shrink-0 items-center justify-end overflow-hidden rounded-full [interpolate-size:allow-keywords] transition-[width,background-color] duration-150 ease-out hover:w-auto focus-visible:w-auto motion-reduce:transition-none"
+
+// Fades in while sliding out from behind the icon.
+const LABEL =
+ "translate-x-2 ps-2 pe-0.5 text-[11px] leading-4 font-medium whitespace-nowrap text-notice opacity-0 transition-[opacity,translate] duration-150 ease-out group-hover:translate-x-0 group-hover:opacity-100 group-focus-visible:translate-x-0 group-focus-visible:opacity-100 motion-reduce:translate-x-0 motion-reduce:transition-none"
+
+// Long enough for the quit to begin; if it was cancelled, the pill comes back.
+const RESTART_SETTLE_MS = 3000
+
+function Slot({ children }: { children: ReactNode }) {
+ return (
+
+ {children}
+
+ )
+}
+
+/**
+ * Beside the right-panel toggle, the one place updates show outside Settings:
+ * a download's progress, then the restart into it. Nothing otherwise, so the
+ * title bar keeps no space for it.
+ */
+export function TitleBarUpdate() {
+ const state = useUpdateState()
+ const [restarting, setRestarting] = useState(false)
+ const settle = useRef
(undefined)
+ useEffect(() => () => window.clearTimeout(settle.current), [])
+
+ if (state.status === "downloading") {
+ const { percent, version } = state
+ return (
+
+
+ {percent === null
+ ? intl.formatMessage({
+ id: "updates_title_bar_downloading_unknown_label",
+ defaultMessage: "Downloading…",
+ })
+ : intl.formatMessage(
+ {
+ id: "updates_title_bar_downloading_label",
+ defaultMessage: "Downloading {percent, number, ::percent}",
+ },
+ { percent: percent / 100 }
+ )}
+
+
+
+
+
+
+ )
+ }
+
+ if (state.status !== "ready") return null
+ const { version } = state
+ const restart = () => {
+ setRestarting(true)
+ void updatesBridge()
+ ?.install()
+ .finally(() => {
+ settle.current = window.setTimeout(
+ () => setRestarting(false),
+ RESTART_SETTLE_MS
+ )
+ })
+ }
+ return (
+
+
+ {intl.formatMessage({
+ id: "updates_title_bar_ready_label",
+ defaultMessage: "Update",
+ })}
+
+
+ {restarting ? (
+
+ ) : (
+
+ )}
+
+
+ )
+}
diff --git a/surfsense_local/frontend/src/features/updates/update-settings.test.tsx b/surfsense_local/frontend/src/features/updates/update-settings.test.tsx
index 156a4372f3..ad452c53c5 100644
--- a/surfsense_local/frontend/src/features/updates/update-settings.test.tsx
+++ b/surfsense_local/frontend/src/features/updates/update-settings.test.tsx
@@ -6,7 +6,7 @@ import { EgressPrompt } from "@/features/egress/egress-prompt"
import { render } from "@/test-utils"
import { stubUpdateBridge as stubBridge } from "./stub-bridge"
-import { UpdateButton, UpdateSettings } from "./update-settings"
+import { UpdateSettings } from "./update-settings"
afterEach(() => {
cleanup()
@@ -59,7 +59,7 @@ describe("UpdateSettings", () => {
expect(await screen.findByText("Checking…")).toBeTruthy()
bridge.push({ status: "up-to-date" })
expect(await screen.findByText("SurfSense is up to date")).toBeTruthy()
- bridge.push({ status: "downloading", version: "1.0.1" })
+ bridge.push({ status: "downloading", version: "1.0.1", percent: null })
expect(await screen.findByText("Downloading 1.0.1…")).toBeTruthy()
bridge.push({ status: "error", message: "net::ERR_INTERNET_DISCONNECTED" })
expect((await screen.findByRole("alert")).textContent).toContain(
@@ -95,30 +95,3 @@ describe("UpdateSettings", () => {
expect(container.textContent).toBe("")
})
})
-
-describe("UpdateButton", () => {
- it("appears only when an update is ready and restarts on click", async () => {
- const bridge = stubBridge({ automatic: true, state: { status: "idle" } })
- const user = userEvent.setup()
- render( )
-
- // No element at all: the title bar keeps no space for it. Queried by role
- // rather than text, since the button is icon-only and has no text content.
- expect(screen.queryByRole("button")).toBeNull()
- bridge.push({ status: "ready", version: "1.0.1" })
-
- const button = await screen.findByRole("button", {
- name: "Restart to install 1.0.1",
- })
- await user.click(button)
- expect(bridge.calls).toEqual(["install"])
- })
-
- it("names the waiting version, the only place it is shown", async () => {
- const bridge = stubBridge({ automatic: true, state: { status: "idle" } })
- render( )
- bridge.push({ status: "ready", version: "0.0.41" })
-
- await screen.findByRole("button", { name: "Restart to install 0.0.41" })
- })
-})
diff --git a/surfsense_local/frontend/src/features/updates/update-settings.tsx b/surfsense_local/frontend/src/features/updates/update-settings.tsx
index 65283e17fb..013e172c82 100644
--- a/surfsense_local/frontend/src/features/updates/update-settings.tsx
+++ b/surfsense_local/frontend/src/features/updates/update-settings.tsx
@@ -1,15 +1,11 @@
import { Button } from "@/components/ui/button"
-import { DownloadCircle02Icon } from "@/components/ui/icons"
-import {
- Tooltip,
- TooltipContent,
- TooltipTrigger,
-} from "@/components/ui/tooltip"
+import { updatesText } from "@/features/privacy-choices/choice-text"
+import { DetailsTooltip } from "@/features/privacy-choices/details-tooltip"
import { updatesLockedText } from "@/features/privacy-choices/locked-text"
import { usePrivacyState } from "@/features/privacy-choices/use-privacy-state"
import { WhatsSentDialog } from "@/features/privacy-choices/whats-sent-dialog"
import { intl } from "@/i18n/intl"
-import type { PrivacyState, UpdateState } from "@/lib/api"
+import type { UpdateState } from "@/lib/api"
import { useCheckForUpdates } from "./use-check-for-updates"
import {
@@ -56,35 +52,6 @@ function statusText(state: UpdateState) {
}
}
-// Where checks go: SurfSense's feed with GitHub behind it, or a policy's mirror.
-function bodyText(state: PrivacyState | null | undefined) {
- // No privacy bridge: an Electron main that still reads GitHub's releases.
- if (state === null)
- return intl.formatMessage({
- id: "updates_settings_github_body",
- defaultMessage:
- "Free updates from GitHub. Once you allow update checks, SurfSense also checks each time it starts.",
- })
- const updates = state?.updates
- if (updates && updates.fallbackHost === null)
- return intl.formatMessage(
- {
- id: "updates_settings_mirror_body",
- defaultMessage:
- "Free updates from {host}, the update server your organization set. Once you allow update checks, SurfSense asks it once a day.",
- },
- { host: updates.host }
- )
- return intl.formatMessage(
- {
- id: "updates_settings_body",
- defaultMessage:
- "Free updates. Once you allow update checks, SurfSense asks {host} once a day, and github.com whenever {host} cannot answer.",
- },
- { host: updates?.host ?? "updates.surfsense.com" }
- )
-}
-
export function UpdateSettings() {
const updates = updatesBridge()
const state = useUpdateState()
@@ -99,20 +66,38 @@ export function UpdateSettings() {
// A package manager's install never asks the feed, so there is no daily
// check to describe and no request to show.
const channelLocked = privacy?.updates.lockedReason === "channel"
+ const described = privacy && !channelLocked ? updatesText(privacy) : null
return (
-
- {intl.formatMessage({
- id: "updates_settings_title",
- defaultMessage: "App updates",
- })}
-
- {channelLocked ? null : (
+
+
+ {intl.formatMessage({
+ id: "updates_settings_title",
+ defaultMessage: "App updates",
+ })}
+
+ {described ? (
+
+ ) : null}
+
+ {described ? (
+
+ {described.summary}
+
+ ) : privacy === null ? (
+ // No privacy bridge: an Electron main that still reads GitHub's releases.
- {bodyText(privacy)}
+ {intl.formatMessage({
+ id: "updates_settings_github_body",
+ defaultMessage:
+ "Free updates from GitHub. Once you allow update checks, SurfSense also checks each time it starts.",
+ })}
- )}
+ ) : null}
{locked ? (
{locked}
) : null}
@@ -167,47 +152,3 @@ export function UpdateSettings() {
)
}
-
-/**
- * Sits in the title bar and appears only once an update has been downloaded
- * and is waiting. Shaped exactly like the right-panel toggle beside it — ghost,
- * same size — so only the color sets it apart.
- *
- * The title bar is `position: fixed`, so this button is out of the document
- * flow: rendering nothing costs no space and cannot disturb the layout around
- * it.
- */
-export function UpdateButton() {
- const state = useUpdateState()
- if (state.status !== "ready") return null
- const label = intl.formatMessage(
- {
- id: "updates_title_bar_restart_tooltip",
- defaultMessage: "Restart to install {version}",
- },
- {
- version: state.version,
- }
- )
- return (
-
- void updatesBridge()?.install()}
- >
-
-
- }
- />
-
- {label}
-
-
- )
-}
diff --git a/surfsense_local/frontend/src/features/updates/use-check-for-updates.ts b/surfsense_local/frontend/src/features/updates/use-check-for-updates.ts
index 111d2bf850..e28a061269 100644
--- a/surfsense_local/frontend/src/features/updates/use-check-for-updates.ts
+++ b/surfsense_local/frontend/src/features/updates/use-check-for-updates.ts
@@ -11,19 +11,23 @@ const GITHUB_HOST = "github.com"
/**
* The one way to check: Settings › About, the sidebar's update row and the app
* menu. Settings › Network promises a check is refused until allowed, so while
- * update checks are off it asks first, naming where the check goes.
+ * update checks are off it asks first, naming where the check goes. Resolves
+ * to whether a check was started.
*/
export function useCheckForUpdates() {
const { prefs, setAutomatic } = useUpdatePrefs()
- return async () => {
+ return async (): Promise
=> {
const updates = updatesBridge()
- if (!updates) return
+ if (!updates) return false
const bridge = privacyBridge()
const privacy = bridge ? await bridge.getState().catch(() => null) : null
// A policy that turned updates off leaves nothing to ask or check.
- if (privacy?.updates.locked) return
+ if (privacy?.updates.locked) return false
const current = prefs ?? (await updates.prefs())
- if (current.automatic) return void updates.check()
+ if (current.automatic) {
+ await updates.check()
+ return true
+ }
const allowed = await askEgress({
destination: "app_updates",
host: bridge ? (privacy?.updates.host ?? UPDATE_HOST) : GITHUB_HOST,
@@ -36,5 +40,6 @@ export function useCheckForUpdates() {
allow: () => setAutomatic(true),
})
if (allowed) await updates.check()
+ return allowed
}
}
diff --git a/surfsense_local/frontend/src/lib/api.ts b/surfsense_local/frontend/src/lib/api.ts
index 62f6126a69..fe9287bf70 100644
--- a/surfsense_local/frontend/src/lib/api.ts
+++ b/surfsense_local/frontend/src/lib/api.ts
@@ -112,7 +112,7 @@ export type UpdateState =
| { status: "idle" }
| { status: "checking" }
| { status: "up-to-date" }
- | { status: "downloading"; version: string }
+ | { status: "downloading"; version: string; percent: number | null }
| { status: "ready"; version: string }
| { status: "error"; message: string }
diff --git a/surfsense_local/frontend/translations/de.json b/surfsense_local/frontend/translations/de.json
index c294318cdd..4acb1ee32c 100644
--- a/surfsense_local/frontend/translations/de.json
+++ b/surfsense_local/frontend/translations/de.json
@@ -242,12 +242,6 @@
"dashboard_footer_license_clock_status": "Uhrzeit stimmt nicht",
"dashboard_footer_license_expired_status": "Lizenz abgelaufen",
"dashboard_footer_license_none_status": "Keine Lizenz",
- "dashboard_footer_update_check_button": "Nach Updates suchen",
- "dashboard_footer_update_checking_status": "Wird geprüft …",
- "dashboard_footer_update_current_status": "Auf dem neuesten Stand",
- "dashboard_footer_update_downloading_status": "Wird heruntergeladen …",
- "dashboard_footer_update_failed_status": "Update-Prüfung fehlgeschlagen",
- "dashboard_footer_update_restart_button": "Neu starten und aktualisieren",
"dashboard_right_panel_aria": "Workspace-Artefakte",
"dashboard_right_panel_hide_aria": "Rechten Bereich ausblenden",
"dashboard_right_panel_hide_tooltip": "Rechten Bereich ausblenden",
@@ -837,16 +831,13 @@
"onboarding_privacy_save_error": "Deine Auswahl konnte nicht gespeichert werden. Versuch es noch einmal.",
"onboarding_privacy_title": "Datenschutz-Auswahl",
"onboarding_privacy_unanswered_status": "Beantworte beide Fragen, um fortzufahren.",
- "onboarding_privacy_updates_body": "Einmal am Tag fragt SurfSense bei {host} nach, ob es eine neuere Version gibt, und sendet dabei deine IP-Adresse, den Namen und die Version der App, dein Betriebssystem und dessen Architektur, aber keine ID. Wenn {host} nicht antwortet, fragt SurfSense github.com und sendet nur deine IP-Adresse und den Namen der App. Eine neuere Version wird automatisch von github.com und githubusercontent.com heruntergeladen, die ebenfalls deine IP-Adresse sehen, und installiert, wenn du neu startest oder SurfSense das nächste Mal beendest.",
- "onboarding_privacy_updates_mirror_body": "Einmal am Tag fragt SurfSense bei {host}, dem Update-Server deiner Organisation, nach, ob es eine neuere Version gibt, und sendet dabei deine IP-Adresse, den Namen und die Version der App, dein Betriebssystem und dessen Architektur, aber keine ID. Eine neuere Version wird automatisch von {host} heruntergeladen und installiert, wenn du neu startest oder SurfSense das nächste Mal beendest.",
"onboarding_privacy_updates_no_button": "Nein",
"onboarding_privacy_updates_title": "Nach Updates suchen",
- "onboarding_privacy_updates_whats_sent_button": "Genau ansehen, was gesendet wird",
+ "onboarding_privacy_updates_whats_sent_button": "Genau zeigen, was gesendet wird",
"onboarding_privacy_updates_yes_button": "Ja",
- "onboarding_privacy_usage_body": "Einmal am Tag teilt SurfSense {host} mit, dass diese Installation genutzt wurde, zusammen mit der Version der App, deinem System, der Sprache der App und der Woche der Installation. Einmal pro Woche kommen Zahlen in Bereichen hinzu: Chats, Agent-Durchläufe, Quellen, Studio-Ergebnisse, Plugin-Aufrufe, ob ein lokales oder ein entferntes Modell geantwortet hat und wie weit die Einrichtung gekommen ist. Der erste Bericht enthält außerdem Arbeitsspeicher und Grafikkarte dieses Computers, ebenfalls in Bereichen. Niemals Dokumente, Prompts, Dateinamen, Modellnamen, Hosts, deinen Lizenzschlüssel oder eine ID.",
"onboarding_privacy_usage_no_button": "Nein",
"onboarding_privacy_usage_title": "Nutzungsstatistiken teilen",
- "onboarding_privacy_usage_whats_sent_button": "Genau ansehen, was gesendet wird",
+ "onboarding_privacy_usage_whats_sent_button": "Genau zeigen, was gesendet wird",
"onboarding_privacy_usage_yes_button": "Ja",
"onboarding_progress_aria": "Einrichtungsschritt {step, number} von {total, number}",
"onboarding_server_option_aria": "Server nutzen",
@@ -1144,7 +1135,14 @@
"privacy_choices_update_user_agent_header_body": "Immer SurfSense, ohne Version",
"privacy_choices_update_version_header_body": "Die Version dieser App, etwa 2.2.4",
"privacy_choices_updates_channel_locked_status": "Updates kommen über {channel, select, scoop {Scoop} aur {das AUR} snap {den Snap Store} flatpak {Flatpak} nix {Nix} apt {dein apt-Repository} other {deinen Paketmanager}}, daher sucht SurfSense nicht selbst danach.",
+ "privacy_choices_updates_details_aria": "Was eine Update-Prüfung sendet",
+ "privacy_choices_updates_details_body": "Einmal am Tag fragt SurfSense bei {host} nach, ob es eine neuere Version gibt, und sendet dabei deine IP-Adresse, den Namen und die Version der App, dein Betriebssystem und dessen Architektur, aber keine ID. Wenn {host} nicht antwortet, fragt SurfSense github.com und sendet nur deine IP-Adresse und den Namen der App. Eine neuere Version wird automatisch von github.com und githubusercontent.com heruntergeladen, die ebenfalls deine IP-Adresse sehen, und installiert, wenn du neu startest oder SurfSense das nächste Mal beendest.",
+ "privacy_choices_updates_mirror_details_body": "Einmal am Tag fragt SurfSense bei {host}, dem Update-Server deiner Organisation, nach, ob es eine neuere Version gibt, und sendet dabei deine IP-Adresse, den Namen und die Version der App, dein Betriebssystem und dessen Architektur, aber keine ID. Eine neuere Version wird automatisch von {host} heruntergeladen und installiert, wenn du neu startest oder SurfSense das nächste Mal beendest.",
+ "privacy_choices_updates_mirror_summary_body": "Fragt einmal am Tag beim Update-Server deiner Organisation nach. Ohne ID.",
"privacy_choices_updates_policy_locked_status": "Die Richtlinie deiner Organisation hat Update-Prüfungen auf diesem Computer ausgeschaltet.",
+ "privacy_choices_updates_summary_body": "Sucht einmal am Tag nach einer neueren Version. Ohne ID.",
+ "privacy_choices_usage_details_aria": "Was Nutzungsstatistiken senden",
+ "privacy_choices_usage_details_body": "Einmal am Tag teilt SurfSense {host} mit, dass diese Installation genutzt wurde, zusammen mit der Version der App, deinem System, der Sprache der App und der Woche der Installation. Einmal pro Woche kommen Zahlen in Bereichen hinzu: Chats, Agent-Durchläufe, Quellen, Studio-Ergebnisse, Plugin-Aufrufe, ob ein lokales oder ein entferntes Modell geantwortet hat und wie weit die Einrichtung gekommen ist. Der erste Bericht enthält außerdem Arbeitsspeicher und Grafikkarte dieses Computers, ebenfalls in Bereichen. Niemals Dokumente, Prompts, Dateinamen, Modellnamen, Hosts, deinen Lizenzschlüssel oder eine ID.",
"privacy_choices_usage_do_not_track_locked_status": "Aus, weil auf diesem Computer DO_NOT_TRACK oder SURFSENSE_NO_TELEMETRY gesetzt ist.",
"privacy_choices_usage_last_sent_aria": "Der zuletzt gesendete Nutzungsbericht",
"privacy_choices_usage_last_sent_label": "Gesendet {time}",
@@ -1156,9 +1154,9 @@
"privacy_choices_usage_policy_locked_status": "Die Richtlinie deiner Organisation hat Nutzungsstatistiken auf diesem Computer ausgeschaltet.",
"privacy_choices_usage_report_body": "Höchstens ein Bericht am Tag geht an {host}, als das JSON unten, ohne Cookies. Jeder Wert ist ein Bereich oder ein festes Wort. Wie jede Anfrage enthält er deine IP-Adresse, die die Logs des Hosting-Anbieters kurz aufbewahren; SurfSense speichert nur das daraus abgeleitete Land.",
"privacy_choices_usage_report_off_body": "Nutzungsstatistiken sind aus, daher wird nichts gesendet. So sähe der nächste Bericht aus, wenn du sie einschaltest.",
- "privacy_choices_usage_settings_body": "Einmal am Tag {host} mitteilen, dass diese Installation genutzt wurde, und einmal pro Woche Zahlen in Bereichen ergänzen, etwa wie viele Chats und Quellen. Niemals Dokumente, Prompts, Dateinamen, Modellnamen, Hosts, deinen Lizenzschlüssel oder eine ID.",
"privacy_choices_usage_settings_title": "Nutzungsstatistiken",
"privacy_choices_usage_settings_whats_sent_button": "Genau zeigen, was gesendet wird",
+ "privacy_choices_usage_summary_body": "Zählt einmal am Tag in Bereichen, wie SurfSense genutzt wird. Niemals deine Dokumente, Prompts oder eine ID.",
"privacy_choices_whats_sent_updates_title": "Was eine Update-Prüfung sendet",
"privacy_choices_whats_sent_usage_title": "Was Nutzungsstatistiken senden",
"resources_breakdown_body": "Was jeder Teil von SurfSense gerade nutzt.",
@@ -1671,19 +1669,24 @@
"studio_xlsx_viewer_tabs_aria": "Arbeitsblätter",
"studio_xlsx_viewer_truncated_body": "{count, plural, one {# Zeile wird angezeigt.} other {Die ersten # Zeilen werden angezeigt.}} Lade die Datei herunter, um die ganze Arbeitsmappe zu sehen.",
"studio_xlsx_viewer_unreadable_error": "Diese Arbeitsmappe konnte nicht geöffnet werden. Lade sie herunter, um sie zu öffnen.",
- "updates_settings_body": "Kostenlose Updates. Sobald du Update-Prüfungen erlaubst, fragt SurfSense einmal am Tag bei {host} nach und bei github.com, wenn {host} nicht antwortet.",
+ "updates_menu_check_failed_toast": "Suche nach Updates fehlgeschlagen",
+ "updates_menu_up_to_date_toast": "SurfSense ist auf dem neuesten Stand",
"updates_settings_check_button": "Jetzt prüfen",
"updates_settings_check_error": "Suche nach Updates fehlgeschlagen: {message}",
"updates_settings_checking_status": "Wird geprüft …",
"updates_settings_downloading_status": "{version} wird heruntergeladen …",
"updates_settings_github_body": "Kostenlose Updates über GitHub. Sobald du Update-Prüfungen erlaubst, prüft SurfSense auch bei jedem Start.",
- "updates_settings_mirror_body": "Kostenlose Updates von {host}, dem Update-Server deiner Organisation. Sobald du Update-Prüfungen erlaubst, fragt SurfSense dort einmal am Tag nach.",
"updates_settings_ready_status": "SurfSense {version} ist bereit. Es wird installiert, wenn du neu startest oder SurfSense das nächste Mal beendest.",
"updates_settings_restart_button": "Neu starten und aktualisieren",
"updates_settings_title": "App-Updates",
"updates_settings_up_to_date_status": "SurfSense ist auf dem neuesten Stand",
"updates_settings_whats_sent_button": "Genau zeigen, was gesendet wird",
- "updates_title_bar_restart_tooltip": "Neu starten, um {version} zu installieren",
+ "updates_title_bar_downloading_aria": "SurfSense {version} wird heruntergeladen",
+ "updates_title_bar_downloading_label": "Wird heruntergeladen: {percent, number, ::percent}",
+ "updates_title_bar_downloading_unknown_label": "Wird heruntergeladen …",
+ "updates_title_bar_ready_label": "Update",
+ "updates_title_bar_restart_aria": "Neu starten, um {version} zu installieren",
+ "updates_title_bar_restarting_aria": "Neustart, um {version} zu installieren",
"workspaces_create_dialog_body": "Mit eigener Quellenbibliothek und eigenen Chats.",
"workspaces_create_dialog_submit_button": "Erstellen",
"workspaces_create_dialog_title": "Workspace erstellen",
diff --git a/surfsense_local/frontend/translations/en.json b/surfsense_local/frontend/translations/en.json
index a346aa7d1d..b8577f8d20 100644
--- a/surfsense_local/frontend/translations/en.json
+++ b/surfsense_local/frontend/translations/en.json
@@ -242,12 +242,6 @@
"dashboard_footer_license_clock_status": "Clock is off",
"dashboard_footer_license_expired_status": "License expired",
"dashboard_footer_license_none_status": "No license",
- "dashboard_footer_update_check_button": "Check for updates",
- "dashboard_footer_update_checking_status": "Checking…",
- "dashboard_footer_update_current_status": "Up to date",
- "dashboard_footer_update_downloading_status": "Downloading…",
- "dashboard_footer_update_failed_status": "Update check failed",
- "dashboard_footer_update_restart_button": "Restart to update",
"dashboard_right_panel_aria": "Workspace artifacts",
"dashboard_right_panel_hide_aria": "Hide right panel",
"dashboard_right_panel_hide_tooltip": "Hide right panel",
@@ -837,16 +831,13 @@
"onboarding_privacy_save_error": "Could not save your choices. Try again.",
"onboarding_privacy_title": "Privacy choices",
"onboarding_privacy_unanswered_status": "Answer both to continue.",
- "onboarding_privacy_updates_body": "Once a day, SurfSense asks {host} whether a newer version exists, sending your IP address, the app’s name and version, your operating system and its architecture, and no ID. If {host} cannot answer, it asks github.com with only your IP address and the app’s name. A newer version downloads by itself from github.com and githubusercontent.com, which also see your IP address, and installs when you restart or the next time you quit.",
- "onboarding_privacy_updates_mirror_body": "Once a day, SurfSense asks {host}, the update server your organization set, whether a newer version exists, sending your IP address, the app’s name and version, your operating system and its architecture, and no ID. A newer version downloads by itself from {host} and installs when you restart or the next time you quit.",
"onboarding_privacy_updates_no_button": "No",
"onboarding_privacy_updates_title": "Check for updates",
- "onboarding_privacy_updates_whats_sent_button": "See exactly what’s sent",
+ "onboarding_privacy_updates_whats_sent_button": "Show exactly what’s sent",
"onboarding_privacy_updates_yes_button": "Yes",
- "onboarding_privacy_usage_body": "Once a day, SurfSense tells {host} this install was used, with the app’s version, your system, the app’s language and the week it was installed. Once a week it adds counts in ranges: chats, agent turns, sources, Studio outputs, plugin calls, whether a local or remote model answered, and how far setup got. Its first report adds this computer’s memory and graphics card, in ranges. Never documents, prompts, file names, model names, hosts, your license key or an ID.",
"onboarding_privacy_usage_no_button": "No",
"onboarding_privacy_usage_title": "Share usage statistics",
- "onboarding_privacy_usage_whats_sent_button": "See exactly what’s sent",
+ "onboarding_privacy_usage_whats_sent_button": "Show exactly what’s sent",
"onboarding_privacy_usage_yes_button": "Yes",
"onboarding_progress_aria": "Onboarding step {step, number} of {total, number}",
"onboarding_server_option_aria": "Use a server",
@@ -1144,7 +1135,14 @@
"privacy_choices_update_user_agent_header_body": "Always SurfSense, with no version",
"privacy_choices_update_version_header_body": "This app’s version, such as 2.2.4",
"privacy_choices_updates_channel_locked_status": "Updates come from {channel, select, scoop {Scoop} aur {the AUR} snap {the Snap Store} flatpak {Flatpak} nix {Nix} apt {your apt repository} other {your package manager}}, so SurfSense doesn’t check for them itself.",
+ "privacy_choices_updates_details_aria": "What an update check sends",
+ "privacy_choices_updates_details_body": "Once a day, SurfSense asks {host} whether a newer version exists, sending your IP address, the app’s name and version, your operating system and its architecture, and no ID. If {host} cannot answer, it asks github.com with only your IP address and the app’s name. A newer version downloads by itself from github.com and githubusercontent.com, which also see your IP address, and installs when you restart or the next time you quit.",
+ "privacy_choices_updates_mirror_details_body": "Once a day, SurfSense asks {host}, the update server your organization set, whether a newer version exists, sending your IP address, the app’s name and version, your operating system and its architecture, and no ID. A newer version downloads by itself from {host} and installs when you restart or the next time you quit.",
+ "privacy_choices_updates_mirror_summary_body": "Checks your organization’s update server once a day. No ID sent.",
"privacy_choices_updates_policy_locked_status": "Your organization’s policy turned update checks off on this computer.",
+ "privacy_choices_updates_summary_body": "Checks for a newer version once a day. No ID sent.",
+ "privacy_choices_usage_details_aria": "What usage statistics send",
+ "privacy_choices_usage_details_body": "Once a day, SurfSense tells {host} this install was used, with the app’s version, your system, the app’s language and the week it was installed. Once a week it adds counts in ranges: chats, agent turns, sources, Studio outputs, plugin calls, whether a local or remote model answered, and how far setup got. Its first report adds this computer’s memory and graphics card, in ranges. Never documents, prompts, file names, model names, hosts, your license key or an ID.",
"privacy_choices_usage_do_not_track_locked_status": "Off, because DO_NOT_TRACK or SURFSENSE_NO_TELEMETRY is set on this computer.",
"privacy_choices_usage_last_sent_aria": "The last usage report sent",
"privacy_choices_usage_last_sent_label": "Sent {time}",
@@ -1156,9 +1154,9 @@
"privacy_choices_usage_policy_locked_status": "Your organization’s policy turned usage statistics off on this computer.",
"privacy_choices_usage_report_body": "At most one report a day goes to {host}, as the JSON below, with no cookies. Every value is a range or a fixed word. Like every request it carries your IP address, which the hosting provider’s logs keep for a short time; SurfSense records only the country derived from it.",
"privacy_choices_usage_report_off_body": "Usage statistics are off, so nothing is sent. This is what the next report would be if you turned them on.",
- "privacy_choices_usage_settings_body": "Once a day, tell {host} this install was used, and once a week add counts in ranges, such as how many chats and sources. Never documents, prompts, file names, model names, hosts, your license key or an ID.",
"privacy_choices_usage_settings_title": "Usage statistics",
"privacy_choices_usage_settings_whats_sent_button": "Show exactly what’s sent",
+ "privacy_choices_usage_summary_body": "A daily count of how SurfSense is used, in ranges. Never your documents, prompts or an ID.",
"privacy_choices_whats_sent_updates_title": "What an update check sends",
"privacy_choices_whats_sent_usage_title": "What usage statistics send",
"resources_breakdown_body": "What each part of SurfSense is using right now.",
@@ -1671,19 +1669,24 @@
"studio_xlsx_viewer_tabs_aria": "Worksheets",
"studio_xlsx_viewer_truncated_body": "Showing the first {count, plural, one {# row} other {# rows}}. Download the file for the full workbook.",
"studio_xlsx_viewer_unreadable_error": "This workbook could not be opened. Download it to open it.",
- "updates_settings_body": "Free updates. Once you allow update checks, SurfSense asks {host} once a day, and github.com whenever {host} cannot answer.",
+ "updates_menu_check_failed_toast": "Couldn’t check for updates",
+ "updates_menu_up_to_date_toast": "SurfSense is up to date",
"updates_settings_check_button": "Check now",
"updates_settings_check_error": "Could not check for updates: {message}",
"updates_settings_checking_status": "Checking…",
"updates_settings_downloading_status": "Downloading {version}…",
"updates_settings_github_body": "Free updates from GitHub. Once you allow update checks, SurfSense also checks each time it starts.",
- "updates_settings_mirror_body": "Free updates from {host}, the update server your organization set. Once you allow update checks, SurfSense asks it once a day.",
"updates_settings_ready_status": "SurfSense {version} is ready. It installs when you restart or the next time you quit.",
"updates_settings_restart_button": "Restart to update",
"updates_settings_title": "App updates",
"updates_settings_up_to_date_status": "SurfSense is up to date",
"updates_settings_whats_sent_button": "Show exactly what’s sent",
- "updates_title_bar_restart_tooltip": "Restart to install {version}",
+ "updates_title_bar_downloading_aria": "Downloading SurfSense {version}",
+ "updates_title_bar_downloading_label": "Downloading {percent, number, ::percent}",
+ "updates_title_bar_downloading_unknown_label": "Downloading…",
+ "updates_title_bar_ready_label": "Update",
+ "updates_title_bar_restart_aria": "Restart to install {version}",
+ "updates_title_bar_restarting_aria": "Restarting to install {version}",
"workspaces_create_dialog_body": "Keep a separate source library and set of chats.",
"workspaces_create_dialog_submit_button": "Create",
"workspaces_create_dialog_title": "Create workspace",
diff --git a/surfsense_local/frontend/translations/es.json b/surfsense_local/frontend/translations/es.json
index a7346bf485..fe38cf358f 100644
--- a/surfsense_local/frontend/translations/es.json
+++ b/surfsense_local/frontend/translations/es.json
@@ -242,12 +242,6 @@
"dashboard_footer_license_clock_status": "Hora incorrecta",
"dashboard_footer_license_expired_status": "Licencia expirada",
"dashboard_footer_license_none_status": "Sin licencia",
- "dashboard_footer_update_check_button": "Buscar actualizaciones",
- "dashboard_footer_update_checking_status": "Comprobando…",
- "dashboard_footer_update_current_status": "Actualizado",
- "dashboard_footer_update_downloading_status": "Descargando…",
- "dashboard_footer_update_failed_status": "Error al buscar actualizaciones",
- "dashboard_footer_update_restart_button": "Reiniciar para actualizar",
"dashboard_right_panel_aria": "Artefactos del espacio de trabajo",
"dashboard_right_panel_hide_aria": "Ocultar el panel derecho",
"dashboard_right_panel_hide_tooltip": "Ocultar el panel derecho",
@@ -837,16 +831,13 @@
"onboarding_privacy_save_error": "No se pudieron guardar tus elecciones. Vuelve a intentarlo.",
"onboarding_privacy_title": "Opciones de privacidad",
"onboarding_privacy_unanswered_status": "Responde las dos para continuar.",
- "onboarding_privacy_updates_body": "Una vez al día, SurfSense pregunta a {host} si hay una versión más reciente, enviando tu dirección IP, el nombre y la versión de la app, tu sistema operativo y su arquitectura, y ningún ID. Si {host} no responde, pregunta a github.com enviando solo tu dirección IP y el nombre de la app. Una versión más reciente se descarga sola desde github.com y githubusercontent.com, que también ven tu dirección IP, y se instala cuando reinicias o la próxima vez que cierras SurfSense.",
- "onboarding_privacy_updates_mirror_body": "Una vez al día, SurfSense pregunta a {host}, el servidor de actualizaciones que configuró tu organización, si hay una versión más reciente, enviando tu dirección IP, el nombre y la versión de la app, tu sistema operativo y su arquitectura, y ningún ID. Una versión más reciente se descarga sola desde {host} y se instala cuando reinicias o la próxima vez que cierras SurfSense.",
"onboarding_privacy_updates_no_button": "No",
"onboarding_privacy_updates_title": "Buscar actualizaciones",
- "onboarding_privacy_updates_whats_sent_button": "Ver exactamente qué se envía",
+ "onboarding_privacy_updates_whats_sent_button": "Mostrar exactamente qué se envía",
"onboarding_privacy_updates_yes_button": "Sí",
- "onboarding_privacy_usage_body": "Una vez al día, SurfSense avisa a {host} de que esta instalación se usó, con la versión de la app, tu sistema, el idioma de la app y la semana en que se instaló. Una vez a la semana añade recuentos en rangos: chats, turnos del agente, fuentes, resultados de Studio, llamadas a plugins, si respondió un modelo local o remoto y hasta dónde llegó la configuración. Su primer informe añade la memoria y la tarjeta gráfica de este equipo, en rangos. Nunca documentos, prompts, nombres de archivo, nombres de modelos, hosts, tu clave de licencia ni un ID.",
"onboarding_privacy_usage_no_button": "No",
"onboarding_privacy_usage_title": "Compartir estadísticas de uso",
- "onboarding_privacy_usage_whats_sent_button": "Ver exactamente qué se envía",
+ "onboarding_privacy_usage_whats_sent_button": "Mostrar exactamente qué se envía",
"onboarding_privacy_usage_yes_button": "Sí",
"onboarding_progress_aria": "Paso {step, number} de {total, number} de la configuración",
"onboarding_server_option_aria": "Usar un servidor",
@@ -1144,7 +1135,14 @@
"privacy_choices_update_user_agent_header_body": "Siempre SurfSense, sin versión",
"privacy_choices_update_version_header_body": "La versión de esta app, como 2.2.4",
"privacy_choices_updates_channel_locked_status": "Las actualizaciones llegan {channel, select, scoop {desde Scoop} aur {desde el AUR} snap {desde la Snap Store} flatpak {desde Flatpak} nix {desde Nix} apt {desde tu repositorio de apt} other {desde tu gestor de paquetes}}, así que SurfSense no las busca por su cuenta.",
+ "privacy_choices_updates_details_aria": "Qué envía una comprobación de actualizaciones",
+ "privacy_choices_updates_details_body": "Una vez al día, SurfSense pregunta a {host} si hay una versión más reciente, enviando tu dirección IP, el nombre y la versión de la app, tu sistema operativo y su arquitectura, y ningún ID. Si {host} no responde, pregunta a github.com enviando solo tu dirección IP y el nombre de la app. Una versión más reciente se descarga sola desde github.com y githubusercontent.com, que también ven tu dirección IP, y se instala cuando reinicias o la próxima vez que cierras SurfSense.",
+ "privacy_choices_updates_mirror_details_body": "Una vez al día, SurfSense pregunta a {host}, el servidor de actualizaciones que configuró tu organización, si hay una versión más reciente, enviando tu dirección IP, el nombre y la versión de la app, tu sistema operativo y su arquitectura, y ningún ID. Una versión más reciente se descarga sola desde {host} y se instala cuando reinicias o la próxima vez que cierras SurfSense.",
+ "privacy_choices_updates_mirror_summary_body": "Consulta el servidor de actualizaciones de tu organización una vez al día. Sin enviar ningún ID.",
"privacy_choices_updates_policy_locked_status": "La política de tu organización desactivó la búsqueda de actualizaciones en este equipo.",
+ "privacy_choices_updates_summary_body": "Busca una versión más reciente una vez al día. Sin enviar ningún ID.",
+ "privacy_choices_usage_details_aria": "Qué envían las estadísticas de uso",
+ "privacy_choices_usage_details_body": "Una vez al día, SurfSense avisa a {host} de que esta instalación se usó, con la versión de la app, tu sistema, el idioma de la app y la semana en que se instaló. Una vez a la semana añade recuentos en rangos: chats, turnos del agente, fuentes, resultados de Studio, llamadas a plugins, si respondió un modelo local o remoto y hasta dónde llegó la configuración. Su primer informe añade la memoria y la tarjeta gráfica de este equipo, en rangos. Nunca documentos, prompts, nombres de archivo, nombres de modelos, hosts, tu clave de licencia ni un ID.",
"privacy_choices_usage_do_not_track_locked_status": "Desactivadas porque DO_NOT_TRACK o SURFSENSE_NO_TELEMETRY está definido en este equipo.",
"privacy_choices_usage_last_sent_aria": "El último informe de uso enviado",
"privacy_choices_usage_last_sent_label": "Enviado {time}",
@@ -1156,9 +1154,9 @@
"privacy_choices_usage_policy_locked_status": "La política de tu organización desactivó las estadísticas de uso en este equipo.",
"privacy_choices_usage_report_body": "Como mucho se envía un informe al día a {host}, como el JSON de abajo, sin cookies. Cada valor es un rango o una palabra fija. Como toda solicitud, lleva tu dirección IP, que los registros del proveedor de alojamiento guardan poco tiempo; SurfSense solo registra el país que se deduce de ella.",
"privacy_choices_usage_report_off_body": "Las estadísticas de uso están desactivadas, así que no se envía nada. Así sería el próximo informe si las activaras.",
- "privacy_choices_usage_settings_body": "Una vez al día, avisar a {host} de que esta instalación se usó, y una vez a la semana añadir recuentos en rangos, como cuántos chats y fuentes. Nunca documentos, prompts, nombres de archivo, nombres de modelos, hosts, tu clave de licencia ni un ID.",
"privacy_choices_usage_settings_title": "Estadísticas de uso",
"privacy_choices_usage_settings_whats_sent_button": "Mostrar exactamente qué se envía",
+ "privacy_choices_usage_summary_body": "Un recuento diario, en rangos, de cómo se usa SurfSense. Nunca tus documentos, prompts ni un ID.",
"privacy_choices_whats_sent_updates_title": "Qué envía una comprobación de actualizaciones",
"privacy_choices_whats_sent_usage_title": "Qué envían las estadísticas de uso",
"resources_breakdown_body": "Lo que usa ahora cada parte de SurfSense.",
@@ -1671,19 +1669,24 @@
"studio_xlsx_viewer_tabs_aria": "Hojas",
"studio_xlsx_viewer_truncated_body": "{count, plural, one {Se muestra # fila.} many {Se muestran las primeras # filas.} other {Se muestran las primeras # filas.}} Descarga el archivo para ver el libro completo.",
"studio_xlsx_viewer_unreadable_error": "No se pudo abrir este libro. Descárgalo para abrirlo.",
- "updates_settings_body": "Actualizaciones gratuitas. Cuando permitas la búsqueda de actualizaciones, SurfSense preguntará a {host} una vez al día, y a github.com cuando {host} no responda.",
+ "updates_menu_check_failed_toast": "No se pudieron buscar actualizaciones",
+ "updates_menu_up_to_date_toast": "SurfSense está actualizado",
"updates_settings_check_button": "Comprobar ahora",
"updates_settings_check_error": "No se pudo buscar actualizaciones: {message}",
"updates_settings_checking_status": "Comprobando…",
"updates_settings_downloading_status": "Descargando {version}…",
"updates_settings_github_body": "Actualizaciones gratuitas desde GitHub. Cuando permitas la búsqueda de actualizaciones, SurfSense también buscará cada vez que se inicie.",
- "updates_settings_mirror_body": "Actualizaciones gratuitas desde {host}, el servidor de actualizaciones que configuró tu organización. Cuando permitas la búsqueda de actualizaciones, SurfSense le preguntará una vez al día.",
"updates_settings_ready_status": "SurfSense {version} está listo. Se instala cuando reinicias o la próxima vez que cierras SurfSense.",
"updates_settings_restart_button": "Reiniciar para actualizar",
"updates_settings_title": "Actualizaciones de la app",
"updates_settings_up_to_date_status": "SurfSense está actualizado",
"updates_settings_whats_sent_button": "Mostrar exactamente qué se envía",
- "updates_title_bar_restart_tooltip": "Reiniciar para instalar {version}",
+ "updates_title_bar_downloading_aria": "Descargando SurfSense {version}",
+ "updates_title_bar_downloading_label": "Descargando {percent, number, ::percent}",
+ "updates_title_bar_downloading_unknown_label": "Descargando…",
+ "updates_title_bar_ready_label": "Actualizar",
+ "updates_title_bar_restart_aria": "Reiniciar para instalar {version}",
+ "updates_title_bar_restarting_aria": "Reiniciando para instalar {version}",
"workspaces_create_dialog_body": "Ten una biblioteca de fuentes y un conjunto de chats aparte.",
"workspaces_create_dialog_submit_button": "Crear",
"workspaces_create_dialog_title": "Crear espacio de trabajo",
diff --git a/surfsense_local/frontend/translations/fr.json b/surfsense_local/frontend/translations/fr.json
index 70f36659e2..1715c4f345 100644
--- a/surfsense_local/frontend/translations/fr.json
+++ b/surfsense_local/frontend/translations/fr.json
@@ -242,12 +242,6 @@
"dashboard_footer_license_clock_status": "Horloge déréglée",
"dashboard_footer_license_expired_status": "Licence expirée",
"dashboard_footer_license_none_status": "Aucune licence",
- "dashboard_footer_update_check_button": "Rechercher des mises à jour",
- "dashboard_footer_update_checking_status": "Vérification…",
- "dashboard_footer_update_current_status": "À jour",
- "dashboard_footer_update_downloading_status": "Téléchargement…",
- "dashboard_footer_update_failed_status": "Échec de la recherche de mises à jour",
- "dashboard_footer_update_restart_button": "Redémarrer pour mettre à jour",
"dashboard_right_panel_aria": "Artefacts de l’espace de travail",
"dashboard_right_panel_hide_aria": "Masquer le panneau de droite",
"dashboard_right_panel_hide_tooltip": "Masquer le panneau de droite",
@@ -837,16 +831,13 @@
"onboarding_privacy_save_error": "Impossible d’enregistrer vos choix. Réessayez.",
"onboarding_privacy_title": "Choix de confidentialité",
"onboarding_privacy_unanswered_status": "Répondez aux deux questions pour continuer.",
- "onboarding_privacy_updates_body": "Une fois par jour, SurfSense demande à {host} s’il existe une version plus récente, en envoyant votre adresse IP, le nom et la version de l’application, votre système d’exploitation et son architecture, et aucun identifiant. Si {host} ne répond pas, il interroge github.com avec seulement votre adresse IP et le nom de l’application. Une version plus récente se télécharge d’elle-même depuis github.com et githubusercontent.com, qui voient aussi votre adresse IP, et s’installe lorsque vous redémarrez ou la prochaine fois que vous quittez SurfSense.",
- "onboarding_privacy_updates_mirror_body": "Une fois par jour, SurfSense demande à {host}, le serveur de mises à jour défini par votre organisation, s’il existe une version plus récente, en envoyant votre adresse IP, le nom et la version de l’application, votre système d’exploitation et son architecture, et aucun identifiant. Une version plus récente se télécharge d’elle-même depuis {host} et s’installe lorsque vous redémarrez ou la prochaine fois que vous quittez SurfSense.",
"onboarding_privacy_updates_no_button": "Non",
"onboarding_privacy_updates_title": "Rechercher les mises à jour",
- "onboarding_privacy_updates_whats_sent_button": "Voir exactement ce qui est envoyé",
+ "onboarding_privacy_updates_whats_sent_button": "Afficher exactement ce qui est envoyé",
"onboarding_privacy_updates_yes_button": "Oui",
- "onboarding_privacy_usage_body": "Une fois par jour, SurfSense indique à {host} que cette installation a été utilisée, avec la version de l’application, votre système, la langue de l’application et la semaine d’installation. Une fois par semaine, il ajoute des nombres par tranches : chats, tours de l’agent, sources, résultats de Studio, appels de plugins, si un modèle local ou distant a répondu et jusqu’où la configuration est allée. Son premier rapport ajoute la mémoire et la carte graphique de cet ordinateur, par tranches. Jamais de documents, de prompts, de noms de fichiers, de noms de modèles, d’hôtes, votre clé de licence ni un identifiant.",
"onboarding_privacy_usage_no_button": "Non",
"onboarding_privacy_usage_title": "Partager des statistiques d’utilisation",
- "onboarding_privacy_usage_whats_sent_button": "Voir exactement ce qui est envoyé",
+ "onboarding_privacy_usage_whats_sent_button": "Afficher exactement ce qui est envoyé",
"onboarding_privacy_usage_yes_button": "Oui",
"onboarding_progress_aria": "Étape de configuration {step, number} sur {total, number}",
"onboarding_server_option_aria": "Utiliser un serveur",
@@ -1144,7 +1135,14 @@
"privacy_choices_update_user_agent_header_body": "Toujours SurfSense, sans version",
"privacy_choices_update_version_header_body": "La version de cette application, par exemple 2.2.4",
"privacy_choices_updates_channel_locked_status": "Les mises à jour viennent {channel, select, scoop {de Scoop} aur {de l’AUR} snap {du Snap Store} flatpak {de Flatpak} nix {de Nix} apt {de votre dépôt apt} other {de votre gestionnaire de paquets}}, donc SurfSense ne les recherche pas lui-même.",
+ "privacy_choices_updates_details_aria": "Ce qu’envoie une vérification des mises à jour",
+ "privacy_choices_updates_details_body": "Une fois par jour, SurfSense demande à {host} s’il existe une version plus récente, en envoyant votre adresse IP, le nom et la version de l’application, votre système d’exploitation et son architecture, et aucun identifiant. Si {host} ne répond pas, il interroge github.com avec seulement votre adresse IP et le nom de l’application. Une version plus récente se télécharge d’elle-même depuis github.com et githubusercontent.com, qui voient aussi votre adresse IP, et s’installe lorsque vous redémarrez ou la prochaine fois que vous quittez SurfSense.",
+ "privacy_choices_updates_mirror_details_body": "Une fois par jour, SurfSense demande à {host}, le serveur de mises à jour défini par votre organisation, s’il existe une version plus récente, en envoyant votre adresse IP, le nom et la version de l’application, votre système d’exploitation et son architecture, et aucun identifiant. Une version plus récente se télécharge d’elle-même depuis {host} et s’installe lorsque vous redémarrez ou la prochaine fois que vous quittez SurfSense.",
+ "privacy_choices_updates_mirror_summary_body": "Interroge le serveur de mises à jour de votre organisation une fois par jour. Aucun identifiant envoyé.",
"privacy_choices_updates_policy_locked_status": "La stratégie de votre organisation a désactivé la recherche de mises à jour sur cet ordinateur.",
+ "privacy_choices_updates_summary_body": "Recherche une version plus récente une fois par jour. Aucun identifiant envoyé.",
+ "privacy_choices_usage_details_aria": "Ce qu’envoient les statistiques d’utilisation",
+ "privacy_choices_usage_details_body": "Une fois par jour, SurfSense indique à {host} que cette installation a été utilisée, avec la version de l’application, votre système, la langue de l’application et la semaine d’installation. Une fois par semaine, il ajoute des nombres par tranches : chats, tours de l’agent, sources, résultats de Studio, appels de plugins, si un modèle local ou distant a répondu et jusqu’où la configuration est allée. Son premier rapport ajoute la mémoire et la carte graphique de cet ordinateur, par tranches. Jamais de documents, de prompts, de noms de fichiers, de noms de modèles, d’hôtes, votre clé de licence ni un identifiant.",
"privacy_choices_usage_do_not_track_locked_status": "Désactivées, car DO_NOT_TRACK ou SURFSENSE_NO_TELEMETRY est défini sur cet ordinateur.",
"privacy_choices_usage_last_sent_aria": "Le dernier rapport d’utilisation envoyé",
"privacy_choices_usage_last_sent_label": "Envoyé {time}",
@@ -1156,9 +1154,9 @@
"privacy_choices_usage_policy_locked_status": "La stratégie de votre organisation a désactivé les statistiques d’utilisation sur cet ordinateur.",
"privacy_choices_usage_report_body": "Au plus un rapport par jour part vers {host}, sous la forme du JSON ci-dessous, sans cookies. Chaque valeur est une tranche ou un mot fixe. Comme toute requête, il porte votre adresse IP, que les journaux de l’hébergeur conservent peu de temps ; SurfSense n’enregistre que le pays qui en est déduit.",
"privacy_choices_usage_report_off_body": "Les statistiques d’utilisation sont désactivées, donc rien n’est envoyé. Voici ce que serait le prochain rapport si vous les activiez.",
- "privacy_choices_usage_settings_body": "Une fois par jour, indiquer à {host} que cette installation a été utilisée, et une fois par semaine ajouter des nombres par tranches, comme le nombre de chats et de sources. Jamais de documents, de prompts, de noms de fichiers, de noms de modèles, d’hôtes, votre clé de licence ni un identifiant.",
"privacy_choices_usage_settings_title": "Statistiques d’utilisation",
"privacy_choices_usage_settings_whats_sent_button": "Afficher exactement ce qui est envoyé",
+ "privacy_choices_usage_summary_body": "Un décompte quotidien, par tranches, de l’utilisation de SurfSense. Jamais vos documents, vos prompts ni un identifiant.",
"privacy_choices_whats_sent_updates_title": "Ce qu’envoie une vérification des mises à jour",
"privacy_choices_whats_sent_usage_title": "Ce qu’envoient les statistiques d’utilisation",
"resources_breakdown_body": "Ce que chaque partie de SurfSense utilise en ce moment.",
@@ -1671,19 +1669,24 @@
"studio_xlsx_viewer_tabs_aria": "Feuilles",
"studio_xlsx_viewer_truncated_body": "{count, plural, one {# ligne affichée.} many {Les # premières lignes sont affichées.} other {Les # premières lignes sont affichées.}} Téléchargez le fichier pour obtenir le classeur complet.",
"studio_xlsx_viewer_unreadable_error": "Ce classeur n’a pas pu être ouvert. Téléchargez-le pour l’ouvrir.",
- "updates_settings_body": "Mises à jour gratuites. Une fois la recherche de mises à jour autorisée, SurfSense interroge {host} une fois par jour, et github.com quand {host} ne répond pas.",
+ "updates_menu_check_failed_toast": "Impossible de rechercher des mises à jour",
+ "updates_menu_up_to_date_toast": "SurfSense est à jour",
"updates_settings_check_button": "Vérifier maintenant",
"updates_settings_check_error": "Impossible de rechercher des mises à jour : {message}",
"updates_settings_checking_status": "Vérification…",
"updates_settings_downloading_status": "Téléchargement de {version}…",
"updates_settings_github_body": "Mises à jour gratuites via GitHub. Une fois la recherche de mises à jour autorisée, SurfSense vérifie aussi à chaque démarrage.",
- "updates_settings_mirror_body": "Mises à jour gratuites depuis {host}, le serveur de mises à jour défini par votre organisation. Une fois la recherche de mises à jour autorisée, SurfSense l’interroge une fois par jour.",
"updates_settings_ready_status": "SurfSense {version} est prêt. Il s’installe lorsque vous redémarrez ou la prochaine fois que vous quittez SurfSense.",
"updates_settings_restart_button": "Redémarrer pour mettre à jour",
"updates_settings_title": "Mises à jour de l’application",
"updates_settings_up_to_date_status": "SurfSense est à jour",
"updates_settings_whats_sent_button": "Afficher exactement ce qui est envoyé",
- "updates_title_bar_restart_tooltip": "Redémarrer pour installer {version}",
+ "updates_title_bar_downloading_aria": "Téléchargement de SurfSense {version}",
+ "updates_title_bar_downloading_label": "Téléchargement {percent, number, ::percent}",
+ "updates_title_bar_downloading_unknown_label": "Téléchargement…",
+ "updates_title_bar_ready_label": "Mettre à jour",
+ "updates_title_bar_restart_aria": "Redémarrer pour installer {version}",
+ "updates_title_bar_restarting_aria": "Redémarrage pour installer {version}",
"workspaces_create_dialog_body": "Conservez une bibliothèque de sources et un ensemble de chats distincts.",
"workspaces_create_dialog_submit_button": "Créer",
"workspaces_create_dialog_title": "Créer un espace de travail",
diff --git a/surfsense_local/frontend/translations/hi.json b/surfsense_local/frontend/translations/hi.json
index 5cd056abc6..5be44a3cea 100644
--- a/surfsense_local/frontend/translations/hi.json
+++ b/surfsense_local/frontend/translations/hi.json
@@ -242,12 +242,6 @@
"dashboard_footer_license_clock_status": "घड़ी गलत है",
"dashboard_footer_license_expired_status": "लाइसेंस समाप्त",
"dashboard_footer_license_none_status": "कोई लाइसेंस नहीं",
- "dashboard_footer_update_check_button": "अपडेट देखें",
- "dashboard_footer_update_checking_status": "जांच हो रही है…",
- "dashboard_footer_update_current_status": "अप टू डेट",
- "dashboard_footer_update_downloading_status": "डाउनलोड हो रहा है…",
- "dashboard_footer_update_failed_status": "अपडेट जांच विफल रही",
- "dashboard_footer_update_restart_button": "अपडेट के लिए रीस्टार्ट करें",
"dashboard_right_panel_aria": "वर्कस्पेस के आर्टिफ़ैक्ट",
"dashboard_right_panel_hide_aria": "दायां पैनल छिपाएं",
"dashboard_right_panel_hide_tooltip": "दायां पैनल छिपाएं",
@@ -837,16 +831,13 @@
"onboarding_privacy_save_error": "आपकी पसंद सहेजी नहीं जा सकी। फिर से कोशिश करें।",
"onboarding_privacy_title": "गोपनीयता विकल्प",
"onboarding_privacy_unanswered_status": "जारी रखने के लिए दोनों का जवाब दें।",
- "onboarding_privacy_updates_body": "SurfSense दिन में एक बार {host} से पूछता है कि कोई नया वर्शन है या नहीं, और आपका IP पता, ऐप का नाम और वर्शन, आपका ऑपरेटिंग सिस्टम और उसका आर्किटेक्चर भेजता है, कोई ID नहीं। अगर {host} जवाब नहीं देता, तो यह सिर्फ़ आपका IP पता और ऐप का नाम भेजकर github.com से पूछता है। नया वर्शन github.com और githubusercontent.com से अपने आप डाउनलोड होता है, जिन्हें भी आपका IP पता दिखता है, और तब इंस्टॉल होता है जब आप रीस्टार्ट करते हैं या अगली बार ऐप बंद करते हैं।",
- "onboarding_privacy_updates_mirror_body": "SurfSense दिन में एक बार आपके संगठन के तय किए अपडेट सर्वर {host} से पूछता है कि कोई नया वर्शन है या नहीं, और आपका IP पता, ऐप का नाम और वर्शन, आपका ऑपरेटिंग सिस्टम और उसका आर्किटेक्चर भेजता है, कोई ID नहीं। नया वर्शन {host} से अपने आप डाउनलोड होता है और तब इंस्टॉल होता है जब आप रीस्टार्ट करते हैं या अगली बार ऐप बंद करते हैं।",
"onboarding_privacy_updates_no_button": "नहीं",
"onboarding_privacy_updates_title": "अपडेट की जांच करें",
- "onboarding_privacy_updates_whats_sent_button": "देखें कि ठीक-ठीक क्या भेजा जाता है",
+ "onboarding_privacy_updates_whats_sent_button": "दिखाएं कि ठीक-ठीक क्या भेजा जाता है",
"onboarding_privacy_updates_yes_button": "हां",
- "onboarding_privacy_usage_body": "SurfSense दिन में एक बार {host} को बताता है कि यह इंस्टॉल इस्तेमाल हुआ, साथ में ऐप का वर्शन, आपका सिस्टम, ऐप की भाषा और इंस्टॉल का हफ़्ता। हफ़्ते में एक बार यह दायरों में गिनती जोड़ता है: चैट, एजेंट के टर्न, स्रोत, Studio के आउटपुट, प्लगइन कॉल, जवाब लोकल मॉडल ने दिया या रिमोट मॉडल ने, और सेटअप कहां तक पहुंचा। पहली रिपोर्ट में इस कंप्यूटर की मेमोरी और ग्राफ़िक्स कार्ड भी दायरों में होते हैं। दस्तावेज़, प्रॉम्प्ट, फ़ाइल के नाम, मॉडल के नाम, होस्ट, आपकी लाइसेंस कुंजी या कोई ID कभी नहीं।",
"onboarding_privacy_usage_no_button": "नहीं",
"onboarding_privacy_usage_title": "उपयोग आंकड़े साझा करें",
- "onboarding_privacy_usage_whats_sent_button": "देखें कि ठीक-ठीक क्या भेजा जाता है",
+ "onboarding_privacy_usage_whats_sent_button": "दिखाएं कि ठीक-ठीक क्या भेजा जाता है",
"onboarding_privacy_usage_yes_button": "हां",
"onboarding_progress_aria": "ऑनबोर्डिंग चरण {total, number} में से {step, number}",
"onboarding_server_option_aria": "सर्वर इस्तेमाल करें",
@@ -1144,7 +1135,14 @@
"privacy_choices_update_user_agent_header_body": "हमेशा SurfSense, बिना वर्शन के",
"privacy_choices_update_version_header_body": "इस ऐप का वर्शन, जैसे 2.2.4",
"privacy_choices_updates_channel_locked_status": "अपडेट {channel, select, scoop {Scoop} aur {AUR} snap {Snap Store} flatpak {Flatpak} nix {Nix} apt {आपकी apt रिपॉज़िटरी} other {आपके पैकेज मैनेजर}} से आते हैं, इसलिए SurfSense खुद इनकी जांच नहीं करता।",
+ "privacy_choices_updates_details_aria": "अपडेट की जांच क्या भेजती है",
+ "privacy_choices_updates_details_body": "SurfSense दिन में एक बार {host} से पूछता है कि कोई नया वर्शन है या नहीं, और आपका IP पता, ऐप का नाम और वर्शन, आपका ऑपरेटिंग सिस्टम और उसका आर्किटेक्चर भेजता है, कोई ID नहीं। अगर {host} जवाब नहीं देता, तो यह सिर्फ़ आपका IP पता और ऐप का नाम भेजकर github.com से पूछता है। नया वर्शन github.com और githubusercontent.com से अपने आप डाउनलोड होता है, जिन्हें भी आपका IP पता दिखता है, और तब इंस्टॉल होता है जब आप रीस्टार्ट करते हैं या अगली बार ऐप बंद करते हैं।",
+ "privacy_choices_updates_mirror_details_body": "SurfSense दिन में एक बार आपके संगठन के तय किए अपडेट सर्वर {host} से पूछता है कि कोई नया वर्शन है या नहीं, और आपका IP पता, ऐप का नाम और वर्शन, आपका ऑपरेटिंग सिस्टम और उसका आर्किटेक्चर भेजता है, कोई ID नहीं। नया वर्शन {host} से अपने आप डाउनलोड होता है और तब इंस्टॉल होता है जब आप रीस्टार्ट करते हैं या अगली बार ऐप बंद करते हैं।",
+ "privacy_choices_updates_mirror_summary_body": "दिन में एक बार आपके संगठन के अपडेट सर्वर से पूछता है। कोई ID नहीं भेजी जाती।",
"privacy_choices_updates_policy_locked_status": "आपके संगठन की नीति ने इस कंप्यूटर पर अपडेट की जांच बंद कर दी है।",
+ "privacy_choices_updates_summary_body": "दिन में एक बार नया वर्शन देखता है। कोई ID नहीं भेजी जाती।",
+ "privacy_choices_usage_details_aria": "उपयोग आंकड़े क्या भेजते हैं",
+ "privacy_choices_usage_details_body": "SurfSense दिन में एक बार {host} को बताता है कि यह इंस्टॉल इस्तेमाल हुआ, साथ में ऐप का वर्शन, आपका सिस्टम, ऐप की भाषा और इंस्टॉल का हफ़्ता। हफ़्ते में एक बार यह दायरों में गिनती जोड़ता है: चैट, एजेंट के टर्न, स्रोत, Studio के आउटपुट, प्लगइन कॉल, जवाब लोकल मॉडल ने दिया या रिमोट मॉडल ने, और सेटअप कहां तक पहुंचा। पहली रिपोर्ट में इस कंप्यूटर की मेमोरी और ग्राफ़िक्स कार्ड भी दायरों में होते हैं। दस्तावेज़, प्रॉम्प्ट, फ़ाइल के नाम, मॉडल के नाम, होस्ट, आपकी लाइसेंस कुंजी या कोई ID कभी नहीं।",
"privacy_choices_usage_do_not_track_locked_status": "बंद है, क्योंकि इस कंप्यूटर पर DO_NOT_TRACK या SURFSENSE_NO_TELEMETRY सेट है।",
"privacy_choices_usage_last_sent_aria": "भेजी गई पिछली उपयोग रिपोर्ट",
"privacy_choices_usage_last_sent_label": "भेजी गई: {time}",
@@ -1156,9 +1154,9 @@
"privacy_choices_usage_policy_locked_status": "आपके संगठन की नीति ने इस कंप्यूटर पर उपयोग आंकड़े बंद कर दिए हैं।",
"privacy_choices_usage_report_body": "{host} को दिन में ज़्यादा से ज़्यादा एक रिपोर्ट जाती है, नीचे दिए JSON के रूप में, बिना कुकी के। हर मान एक दायरा या तय शब्द होता है। हर अनुरोध की तरह इसमें आपका IP पता होता है, जिसे होस्टिंग प्रोवाइडर के लॉग थोड़े समय तक रखते हैं; SurfSense सिर्फ़ इससे निकाला गया देश दर्ज करता है।",
"privacy_choices_usage_report_off_body": "उपयोग आंकड़े बंद हैं, इसलिए कुछ नहीं भेजा जाता। अगर आप इन्हें चालू करें तो अगली रिपोर्ट ऐसी होगी।",
- "privacy_choices_usage_settings_body": "दिन में एक बार {host} को बताएं कि यह इंस्टॉल इस्तेमाल हुआ, और हफ़्ते में एक बार दायरों में गिनती जोड़ें, जैसे कितनी चैट और कितने स्रोत। दस्तावेज़, प्रॉम्प्ट, फ़ाइल के नाम, मॉडल के नाम, होस्ट, आपकी लाइसेंस कुंजी या कोई ID कभी नहीं।",
"privacy_choices_usage_settings_title": "उपयोग आंकड़े",
"privacy_choices_usage_settings_whats_sent_button": "दिखाएं कि ठीक-ठीक क्या भेजा जाता है",
+ "privacy_choices_usage_summary_body": "SurfSense के इस्तेमाल की रोज़ की गिनती, दायरों में। आपके दस्तावेज़, प्रॉम्प्ट या कोई ID कभी नहीं।",
"privacy_choices_whats_sent_updates_title": "अपडेट की जांच क्या भेजती है",
"privacy_choices_whats_sent_usage_title": "उपयोग आंकड़े क्या भेजते हैं",
"resources_breakdown_body": "SurfSense का हर हिस्सा अभी क्या इस्तेमाल कर रहा है।",
@@ -1671,19 +1669,24 @@
"studio_xlsx_viewer_tabs_aria": "वर्कशीट",
"studio_xlsx_viewer_truncated_body": "{count, plural, one {पहली # पंक्ति दिखाई जा रही है} other {पहली # पंक्तियां दिखाई जा रही हैं}}। पूरी वर्कबुक के लिए फ़ाइल डाउनलोड करें।",
"studio_xlsx_viewer_unreadable_error": "यह वर्कबुक खोली नहीं जा सकी। इसे खोलने के लिए डाउनलोड करें।",
- "updates_settings_body": "मुफ़्त अपडेट। अपडेट की जांच की अनुमति देने के बाद, SurfSense दिन में एक बार {host} से पूछता है, और जब {host} जवाब न दे तो github.com से।",
+ "updates_menu_check_failed_toast": "अपडेट की जांच नहीं हो सकी",
+ "updates_menu_up_to_date_toast": "SurfSense अप टू डेट है",
"updates_settings_check_button": "अभी जांचें",
"updates_settings_check_error": "अपडेट की जांच नहीं हो सकी: {message}",
"updates_settings_checking_status": "जांच हो रही है…",
"updates_settings_downloading_status": "{version} डाउनलोड हो रहा है…",
"updates_settings_github_body": "GitHub से मुफ़्त अपडेट। अपडेट की जांच की अनुमति देने के बाद, SurfSense हर बार शुरू होने पर भी जांच करता है।",
- "updates_settings_mirror_body": "आपके संगठन के तय किए अपडेट सर्वर {host} से मुफ़्त अपडेट। अपडेट की जांच की अनुमति देने के बाद, SurfSense दिन में एक बार उससे पूछता है।",
"updates_settings_ready_status": "SurfSense {version} तैयार है। यह रीस्टार्ट करने पर या अगली बार ऐप बंद करने पर इंस्टॉल होगा।",
"updates_settings_restart_button": "अपडेट के लिए रीस्टार्ट करें",
"updates_settings_title": "ऐप अपडेट",
"updates_settings_up_to_date_status": "SurfSense अप टू डेट है",
"updates_settings_whats_sent_button": "दिखाएं कि ठीक-ठीक क्या भेजा जाता है",
- "updates_title_bar_restart_tooltip": "{version} इंस्टॉल करने के लिए रीस्टार्ट करें",
+ "updates_title_bar_downloading_aria": "SurfSense {version} डाउनलोड हो रहा है",
+ "updates_title_bar_downloading_label": "डाउनलोड हो रहा है {percent, number, ::percent}",
+ "updates_title_bar_downloading_unknown_label": "डाउनलोड हो रहा है…",
+ "updates_title_bar_ready_label": "अपडेट करें",
+ "updates_title_bar_restart_aria": "{version} इंस्टॉल करने के लिए रीस्टार्ट करें",
+ "updates_title_bar_restarting_aria": "{version} इंस्टॉल करने के लिए रीस्टार्ट हो रहा है",
"workspaces_create_dialog_body": "अलग स्रोत लाइब्रेरी और चैट का सेट रखें।",
"workspaces_create_dialog_submit_button": "बनाएं",
"workspaces_create_dialog_title": "वर्कस्पेस बनाएं",
diff --git a/surfsense_local/frontend/translations/ja.json b/surfsense_local/frontend/translations/ja.json
index fb883d9278..5a9d99c10d 100644
--- a/surfsense_local/frontend/translations/ja.json
+++ b/surfsense_local/frontend/translations/ja.json
@@ -242,12 +242,6 @@
"dashboard_footer_license_clock_status": "時計がずれています",
"dashboard_footer_license_expired_status": "ライセンス期限切れ",
"dashboard_footer_license_none_status": "ライセンスなし",
- "dashboard_footer_update_check_button": "アップデートを確認",
- "dashboard_footer_update_checking_status": "確認中…",
- "dashboard_footer_update_current_status": "最新です",
- "dashboard_footer_update_downloading_status": "ダウンロード中…",
- "dashboard_footer_update_failed_status": "アップデートの確認に失敗しました",
- "dashboard_footer_update_restart_button": "再起動してアップデート",
"dashboard_right_panel_aria": "ワークスペースの生成物",
"dashboard_right_panel_hide_aria": "右パネルを隠す",
"dashboard_right_panel_hide_tooltip": "右パネルを隠す",
@@ -837,16 +831,13 @@
"onboarding_privacy_save_error": "選択内容を保存できませんでした。もう一度お試しください。",
"onboarding_privacy_title": "プライバシーの選択",
"onboarding_privacy_unanswered_status": "続行するには両方に答えてください。",
- "onboarding_privacy_updates_body": "SurfSenseは1日に1回、新しいバージョンがあるかを{host}に問い合わせます。送信するのはIPアドレス、アプリの名前とバージョン、OSとそのアーキテクチャで、IDは送信しません。{host}が応答できない場合は、IPアドレスとアプリの名前だけを送ってgithub.comに問い合わせます。新しいバージョンはgithub.comとgithubusercontent.comから自動でダウンロードされ(どちらにもIPアドレスが伝わります)、再起動したとき、または次に終了したときにインストールされます。",
- "onboarding_privacy_updates_mirror_body": "SurfSenseは1日に1回、組織が設定したアップデートサーバー{host}に新しいバージョンがあるかを問い合わせます。送信するのはIPアドレス、アプリの名前とバージョン、OSとそのアーキテクチャで、IDは送信しません。新しいバージョンは{host}から自動でダウンロードされ、再起動したとき、または次に終了したときにインストールされます。",
"onboarding_privacy_updates_no_button": "いいえ",
"onboarding_privacy_updates_title": "アップデートを確認",
- "onboarding_privacy_updates_whats_sent_button": "送信される内容を正確に見る",
+ "onboarding_privacy_updates_whats_sent_button": "送信される内容を正確に表示",
"onboarding_privacy_updates_yes_button": "はい",
- "onboarding_privacy_usage_body": "SurfSenseは1日に1回、このインストールが使われたことを{host}に伝えます。その際、アプリのバージョン、システム、アプリの言語、インストールした週も送ります。週に1回は範囲で表した件数も加えます。チャット、エージェントのターン、ソース、Studioの出力、プラグインの呼び出し、ローカルとリモートのどちらのモデルが回答したか、セットアップがどこまで進んだかです。最初のレポートには、このコンピューターのメモリとグラフィックカードも範囲で含まれます。ドキュメント、プロンプト、ファイル名、モデル名、ホスト、ライセンスキー、IDは決して送信しません。",
"onboarding_privacy_usage_no_button": "いいえ",
"onboarding_privacy_usage_title": "利用統計を共有",
- "onboarding_privacy_usage_whats_sent_button": "送信される内容を正確に見る",
+ "onboarding_privacy_usage_whats_sent_button": "送信される内容を正確に表示",
"onboarding_privacy_usage_yes_button": "はい",
"onboarding_progress_aria": "オンボーディング ステップ{step, number}/{total, number}",
"onboarding_server_option_aria": "サーバーを使用",
@@ -1144,7 +1135,14 @@
"privacy_choices_update_user_agent_header_body": "常にSurfSense(バージョンなし)",
"privacy_choices_update_version_header_body": "このアプリのバージョン(例: 2.2.4)",
"privacy_choices_updates_channel_locked_status": "アップデートは{channel, select, scoop {Scoop} aur {AUR} snap {Snap Store} flatpak {Flatpak} nix {Nix} apt {aptリポジトリ} other {パッケージマネージャー}}から届くため、SurfSense自身では確認しません。",
+ "privacy_choices_updates_details_aria": "アップデートの確認で送信される内容",
+ "privacy_choices_updates_details_body": "SurfSenseは1日に1回、新しいバージョンがあるかを{host}に問い合わせます。送信するのはIPアドレス、アプリの名前とバージョン、OSとそのアーキテクチャで、IDは送信しません。{host}が応答できない場合は、IPアドレスとアプリの名前だけを送ってgithub.comに問い合わせます。新しいバージョンはgithub.comとgithubusercontent.comから自動でダウンロードされ(どちらにもIPアドレスが伝わります)、再起動したとき、または次に終了したときにインストールされます。",
+ "privacy_choices_updates_mirror_details_body": "SurfSenseは1日に1回、組織が設定したアップデートサーバー{host}に新しいバージョンがあるかを問い合わせます。送信するのはIPアドレス、アプリの名前とバージョン、OSとそのアーキテクチャで、IDは送信しません。新しいバージョンは{host}から自動でダウンロードされ、再起動したとき、または次に終了したときにインストールされます。",
+ "privacy_choices_updates_mirror_summary_body": "1日に1回、組織のアップデートサーバーに確認します。IDは送信しません。",
"privacy_choices_updates_policy_locked_status": "組織のポリシーにより、このコンピューターではアップデートの確認がオフになっています。",
+ "privacy_choices_updates_summary_body": "1日に1回、新しいバージョンを確認します。IDは送信しません。",
+ "privacy_choices_usage_details_aria": "利用統計で送信される内容",
+ "privacy_choices_usage_details_body": "SurfSenseは1日に1回、このインストールが使われたことを{host}に伝えます。その際、アプリのバージョン、システム、アプリの言語、インストールした週も送ります。週に1回は範囲で表した件数も加えます。チャット、エージェントのターン、ソース、Studioの出力、プラグインの呼び出し、ローカルとリモートのどちらのモデルが回答したか、セットアップがどこまで進んだかです。最初のレポートには、このコンピューターのメモリとグラフィックカードも範囲で含まれます。ドキュメント、プロンプト、ファイル名、モデル名、ホスト、ライセンスキー、IDは決して送信しません。",
"privacy_choices_usage_do_not_track_locked_status": "このコンピューターでDO_NOT_TRACKまたはSURFSENSE_NO_TELEMETRYが設定されているため、オフになっています。",
"privacy_choices_usage_last_sent_aria": "最後に送信した利用状況レポート",
"privacy_choices_usage_last_sent_label": "送信: {time}",
@@ -1156,9 +1154,9 @@
"privacy_choices_usage_policy_locked_status": "組織のポリシーにより、このコンピューターでは利用統計がオフになっています。",
"privacy_choices_usage_report_body": "{host}には1日に最大1回、下のJSONのとおりのレポートがCookieなしで送られます。値はすべて範囲か決まった語です。ほかのリクエストと同じくIPアドレスが含まれ、ホスティング事業者のログに短期間残ります。SurfSenseが記録するのは、そこから割り出した国だけです。",
"privacy_choices_usage_report_off_body": "利用統計はオフなので、何も送信されません。オンにした場合の次のレポートは次のとおりです。",
- "privacy_choices_usage_settings_body": "1日に1回、このインストールが使われたことを{host}に伝え、週に1回はチャットやソースの数などの件数を範囲で加えます。ドキュメント、プロンプト、ファイル名、モデル名、ホスト、ライセンスキー、IDは決して送信しません。",
"privacy_choices_usage_settings_title": "利用統計",
"privacy_choices_usage_settings_whats_sent_button": "送信される内容を正確に表示",
+ "privacy_choices_usage_summary_body": "SurfSenseの使われ方を範囲で表した1日ごとの件数です。ドキュメント、プロンプト、IDは決して送信しません。",
"privacy_choices_whats_sent_updates_title": "アップデートの確認で送信される内容",
"privacy_choices_whats_sent_usage_title": "利用統計で送信される内容",
"resources_breakdown_body": "SurfSenseの各部分が現在使っている量です。",
@@ -1671,19 +1669,24 @@
"studio_xlsx_viewer_tabs_aria": "ワークシート",
"studio_xlsx_viewer_truncated_body": "{count, plural, other {最初の#行を表示しています。}}ブック全体を見るにはファイルをダウンロードしてください。",
"studio_xlsx_viewer_unreadable_error": "このブックを開けませんでした。ダウンロードして開いてください。",
- "updates_settings_body": "無料でアップデートできます。アップデートの確認を許可すると、SurfSenseは1日に1回{host}に問い合わせ、{host}が応答できないときはgithub.comに問い合わせます。",
+ "updates_menu_check_failed_toast": "アップデートを確認できませんでした",
+ "updates_menu_up_to_date_toast": "SurfSenseは最新です",
"updates_settings_check_button": "今すぐ確認",
"updates_settings_check_error": "アップデートを確認できませんでした: {message}",
"updates_settings_checking_status": "確認中…",
"updates_settings_downloading_status": "{version}をダウンロード中…",
"updates_settings_github_body": "GitHubから無料でアップデートできます。アップデートの確認を許可すると、SurfSenseは起動するたびにも確認します。",
- "updates_settings_mirror_body": "組織が設定したアップデートサーバー{host}から無料でアップデートできます。アップデートの確認を許可すると、SurfSenseは1日に1回そこに問い合わせます。",
"updates_settings_ready_status": "SurfSense {version}の準備ができました。再起動したとき、または次に終了したときにインストールされます。",
"updates_settings_restart_button": "再起動してアップデート",
"updates_settings_title": "アプリのアップデート",
"updates_settings_up_to_date_status": "SurfSenseは最新です",
"updates_settings_whats_sent_button": "送信される内容を正確に表示",
- "updates_title_bar_restart_tooltip": "再起動して{version}をインストール",
+ "updates_title_bar_downloading_aria": "SurfSense {version}をダウンロード中",
+ "updates_title_bar_downloading_label": "ダウンロード中 {percent, number, ::percent}",
+ "updates_title_bar_downloading_unknown_label": "ダウンロード中…",
+ "updates_title_bar_ready_label": "アップデート",
+ "updates_title_bar_restart_aria": "再起動して{version}をインストール",
+ "updates_title_bar_restarting_aria": "再起動して{version}をインストールしています",
"workspaces_create_dialog_body": "ソースライブラリとチャットを別々に管理できます。",
"workspaces_create_dialog_submit_button": "作成",
"workspaces_create_dialog_title": "ワークスペースを作成",
diff --git a/surfsense_local/frontend/translations/ko.json b/surfsense_local/frontend/translations/ko.json
index 8680ab3523..aa80d9043b 100644
--- a/surfsense_local/frontend/translations/ko.json
+++ b/surfsense_local/frontend/translations/ko.json
@@ -242,12 +242,6 @@
"dashboard_footer_license_clock_status": "시계가 맞지 않음",
"dashboard_footer_license_expired_status": "라이선스 만료",
"dashboard_footer_license_none_status": "라이선스 없음",
- "dashboard_footer_update_check_button": "업데이트 확인",
- "dashboard_footer_update_checking_status": "확인 중…",
- "dashboard_footer_update_current_status": "최신 상태",
- "dashboard_footer_update_downloading_status": "다운로드 중…",
- "dashboard_footer_update_failed_status": "업데이트 확인 실패",
- "dashboard_footer_update_restart_button": "다시 시작하여 업데이트",
"dashboard_right_panel_aria": "워크스페이스 생성물",
"dashboard_right_panel_hide_aria": "오른쪽 패널 숨기기",
"dashboard_right_panel_hide_tooltip": "오른쪽 패널 숨기기",
@@ -837,13 +831,10 @@
"onboarding_privacy_save_error": "선택을 저장할 수 없습니다. 다시 시도하세요.",
"onboarding_privacy_title": "개인정보 선택",
"onboarding_privacy_unanswered_status": "계속하려면 두 가지 모두 답하세요.",
- "onboarding_privacy_updates_body": "SurfSense는 하루에 한 번 {host}에 새 버전이 있는지 묻고, IP 주소, 앱 이름과 버전, 운영 체제와 그 아키텍처를 보내며 ID는 보내지 않습니다. {host}이(가) 응답하지 않으면 IP 주소와 앱 이름만 보내 github.com에 묻습니다. 새 버전은 github.com과 githubusercontent.com에서 자동으로 다운로드되며 이곳에도 IP 주소가 전달되고, 다시 시작하거나 다음에 앱을 종료할 때 설치됩니다.",
- "onboarding_privacy_updates_mirror_body": "SurfSense는 하루에 한 번 조직이 지정한 업데이트 서버 {host}에 새 버전이 있는지 묻고, IP 주소, 앱 이름과 버전, 운영 체제와 그 아키텍처를 보내며 ID는 보내지 않습니다. 새 버전은 {host}에서 자동으로 다운로드되며, 다시 시작하거나 다음에 앱을 종료할 때 설치됩니다.",
"onboarding_privacy_updates_no_button": "아니요",
"onboarding_privacy_updates_title": "업데이트 확인",
"onboarding_privacy_updates_whats_sent_button": "전송되는 내용 정확히 보기",
"onboarding_privacy_updates_yes_button": "예",
- "onboarding_privacy_usage_body": "SurfSense는 하루에 한 번 이 설치가 사용되었다는 사실을 앱 버전, 시스템, 앱 언어, 설치한 주와 함께 {host}에 알립니다. 일주일에 한 번은 범위로 나타낸 횟수를 더합니다. 채팅, 에이전트 턴, 소스, Studio 결과물, 플러그인 호출, 로컬 모델과 원격 모델 중 무엇이 답했는지, 설정이 어디까지 진행되었는지입니다. 첫 보고서에는 이 컴퓨터의 메모리와 그래픽 카드도 범위로 포함됩니다. 문서, 프롬프트, 파일 이름, 모델 이름, 호스트, 라이선스 키, ID는 절대 보내지 않습니다.",
"onboarding_privacy_usage_no_button": "아니요",
"onboarding_privacy_usage_title": "사용 통계 공유",
"onboarding_privacy_usage_whats_sent_button": "전송되는 내용 정확히 보기",
@@ -1144,7 +1135,14 @@
"privacy_choices_update_user_agent_header_body": "항상 SurfSense, 버전 없음",
"privacy_choices_update_version_header_body": "이 앱의 버전(예: 2.2.4)",
"privacy_choices_updates_channel_locked_status": "업데이트는 {channel, select, scoop {Scoop} aur {AUR} snap {Snap Store} flatpak {Flatpak} nix {Nix} apt {apt 저장소} other {패키지 관리자}}에서 제공되므로 SurfSense가 직접 확인하지 않습니다.",
+ "privacy_choices_updates_details_aria": "업데이트 확인이 보내는 내용",
+ "privacy_choices_updates_details_body": "SurfSense는 하루에 한 번 {host}에 새 버전이 있는지 묻고, IP 주소, 앱 이름과 버전, 운영 체제와 그 아키텍처를 보내며 ID는 보내지 않습니다. {host}이(가) 응답하지 않으면 IP 주소와 앱 이름만 보내 github.com에 묻습니다. 새 버전은 github.com과 githubusercontent.com에서 자동으로 다운로드되며 이곳에도 IP 주소가 전달되고, 다시 시작하거나 다음에 앱을 종료할 때 설치됩니다.",
+ "privacy_choices_updates_mirror_details_body": "SurfSense는 하루에 한 번 조직이 지정한 업데이트 서버 {host}에 새 버전이 있는지 묻고, IP 주소, 앱 이름과 버전, 운영 체제와 그 아키텍처를 보내며 ID는 보내지 않습니다. 새 버전은 {host}에서 자동으로 다운로드되며, 다시 시작하거나 다음에 앱을 종료할 때 설치됩니다.",
+ "privacy_choices_updates_mirror_summary_body": "하루에 한 번 조직의 업데이트 서버에 확인합니다. ID는 보내지 않습니다.",
"privacy_choices_updates_policy_locked_status": "조직 정책에 따라 이 컴퓨터에서는 업데이트 확인이 꺼져 있습니다.",
+ "privacy_choices_updates_summary_body": "하루에 한 번 새 버전을 확인합니다. ID는 보내지 않습니다.",
+ "privacy_choices_usage_details_aria": "사용 통계가 보내는 내용",
+ "privacy_choices_usage_details_body": "SurfSense는 하루에 한 번 이 설치가 사용되었다는 사실을 앱 버전, 시스템, 앱 언어, 설치한 주와 함께 {host}에 알립니다. 일주일에 한 번은 범위로 나타낸 횟수를 더합니다. 채팅, 에이전트 턴, 소스, Studio 결과물, 플러그인 호출, 로컬 모델과 원격 모델 중 무엇이 답했는지, 설정이 어디까지 진행되었는지입니다. 첫 보고서에는 이 컴퓨터의 메모리와 그래픽 카드도 범위로 포함됩니다. 문서, 프롬프트, 파일 이름, 모델 이름, 호스트, 라이선스 키, ID는 절대 보내지 않습니다.",
"privacy_choices_usage_do_not_track_locked_status": "이 컴퓨터에 DO_NOT_TRACK 또는 SURFSENSE_NO_TELEMETRY가 설정되어 있어 꺼져 있습니다.",
"privacy_choices_usage_last_sent_aria": "마지막으로 보낸 사용 보고서",
"privacy_choices_usage_last_sent_label": "보낸 시각: {time}",
@@ -1156,9 +1154,9 @@
"privacy_choices_usage_policy_locked_status": "조직 정책에 따라 이 컴퓨터에서는 사용 통계가 꺼져 있습니다.",
"privacy_choices_usage_report_body": "{host}에는 하루 최대 한 번, 아래 JSON 그대로의 보고서가 쿠키 없이 전송됩니다. 모든 값은 범위이거나 정해진 단어입니다. 다른 모든 요청처럼 IP 주소가 포함되며, 호스팅 제공업체의 로그에 잠시 보관됩니다. SurfSense는 이를 바탕으로 판단한 국가만 기록합니다.",
"privacy_choices_usage_report_off_body": "사용 통계가 꺼져 있어 아무것도 전송되지 않습니다. 켜면 다음 보고서는 다음과 같습니다.",
- "privacy_choices_usage_settings_body": "하루에 한 번 이 설치가 사용되었다고 {host}에 알리고, 일주일에 한 번 채팅과 소스 수 같은 횟수를 범위로 더합니다. 문서, 프롬프트, 파일 이름, 모델 이름, 호스트, 라이선스 키, ID는 절대 보내지 않습니다.",
"privacy_choices_usage_settings_title": "사용 통계",
"privacy_choices_usage_settings_whats_sent_button": "전송되는 내용 정확히 보기",
+ "privacy_choices_usage_summary_body": "SurfSense 사용량을 범위로 나타낸 하루 단위 횟수입니다. 문서, 프롬프트, ID는 절대 보내지 않습니다.",
"privacy_choices_whats_sent_updates_title": "업데이트 확인이 보내는 내용",
"privacy_choices_whats_sent_usage_title": "사용 통계가 보내는 내용",
"resources_breakdown_body": "SurfSense의 각 부분이 지금 사용하는 양입니다.",
@@ -1671,19 +1669,24 @@
"studio_xlsx_viewer_tabs_aria": "워크시트",
"studio_xlsx_viewer_truncated_body": "{count, plural, other {처음 #행}}을 표시하고 있습니다. 전체 통합 문서를 보려면 파일을 다운로드하세요.",
"studio_xlsx_viewer_unreadable_error": "이 통합 문서를 열지 못했습니다. 다운로드해서 여세요.",
- "updates_settings_body": "무료 업데이트입니다. 업데이트 확인을 허용하면 SurfSense는 하루에 한 번 {host}에 묻고, {host}이(가) 응답하지 않으면 github.com에 묻습니다.",
+ "updates_menu_check_failed_toast": "업데이트를 확인할 수 없습니다",
+ "updates_menu_up_to_date_toast": "SurfSense는 최신 상태입니다",
"updates_settings_check_button": "지금 확인",
"updates_settings_check_error": "업데이트를 확인하지 못했습니다: {message}",
"updates_settings_checking_status": "확인 중…",
"updates_settings_downloading_status": "{version} 다운로드 중…",
"updates_settings_github_body": "GitHub에서 무료로 업데이트할 수 있습니다. 업데이트 확인을 허용하면 SurfSense는 시작할 때마다 확인도 합니다.",
- "updates_settings_mirror_body": "조직이 지정한 업데이트 서버 {host}에서 무료로 업데이트합니다. 업데이트 확인을 허용하면 SurfSense는 하루에 한 번 그곳에 묻습니다.",
"updates_settings_ready_status": "SurfSense {version} 준비가 완료되었습니다. 다시 시작하거나 다음에 종료할 때 설치됩니다.",
"updates_settings_restart_button": "다시 시작하여 업데이트",
"updates_settings_title": "앱 업데이트",
"updates_settings_up_to_date_status": "SurfSense는 최신 상태입니다",
"updates_settings_whats_sent_button": "전송되는 내용 정확히 보기",
- "updates_title_bar_restart_tooltip": "다시 시작하여 {version} 설치",
+ "updates_title_bar_downloading_aria": "SurfSense {version} 다운로드 중",
+ "updates_title_bar_downloading_label": "다운로드 중 {percent, number, ::percent}",
+ "updates_title_bar_downloading_unknown_label": "다운로드 중…",
+ "updates_title_bar_ready_label": "업데이트",
+ "updates_title_bar_restart_aria": "다시 시작하여 {version} 설치",
+ "updates_title_bar_restarting_aria": "{version} 설치를 위해 다시 시작하는 중",
"workspaces_create_dialog_body": "소스 라이브러리와 채팅을 따로 관리합니다.",
"workspaces_create_dialog_submit_button": "만들기",
"workspaces_create_dialog_title": "워크스페이스 만들기",
diff --git a/surfsense_local/frontend/translations/pt-BR.json b/surfsense_local/frontend/translations/pt-BR.json
index f145cfc979..aa3bfa1460 100644
--- a/surfsense_local/frontend/translations/pt-BR.json
+++ b/surfsense_local/frontend/translations/pt-BR.json
@@ -242,12 +242,6 @@
"dashboard_footer_license_clock_status": "Relógio desajustado",
"dashboard_footer_license_expired_status": "Licença expirada",
"dashboard_footer_license_none_status": "Sem licença",
- "dashboard_footer_update_check_button": "Verificar atualizações",
- "dashboard_footer_update_checking_status": "Verificando…",
- "dashboard_footer_update_current_status": "Atualizado",
- "dashboard_footer_update_downloading_status": "Baixando…",
- "dashboard_footer_update_failed_status": "Falha ao verificar atualizações",
- "dashboard_footer_update_restart_button": "Reiniciar para atualizar",
"dashboard_right_panel_aria": "Artefatos do workspace",
"dashboard_right_panel_hide_aria": "Ocultar painel direito",
"dashboard_right_panel_hide_tooltip": "Ocultar painel direito",
@@ -837,16 +831,13 @@
"onboarding_privacy_save_error": "Não foi possível salvar suas escolhas. Tente novamente.",
"onboarding_privacy_title": "Escolhas de privacidade",
"onboarding_privacy_unanswered_status": "Responda às duas para continuar.",
- "onboarding_privacy_updates_body": "Uma vez por dia, o SurfSense pergunta a {host} se existe uma versão mais nova, enviando seu endereço IP, o nome e a versão do app, seu sistema operacional e a arquitetura dele, e nenhum ID. Se {host} não responder, ele pergunta ao github.com enviando só seu endereço IP e o nome do app. Uma versão mais nova é baixada sozinha do github.com e do githubusercontent.com, que também veem seu endereço IP, e instalada quando você reinicia ou na próxima vez que fecha o SurfSense.",
- "onboarding_privacy_updates_mirror_body": "Uma vez por dia, o SurfSense pergunta a {host}, o servidor de atualizações definido pela sua organização, se existe uma versão mais nova, enviando seu endereço IP, o nome e a versão do app, seu sistema operacional e a arquitetura dele, e nenhum ID. Uma versão mais nova é baixada sozinha de {host} e instalada quando você reinicia ou na próxima vez que fecha o SurfSense.",
"onboarding_privacy_updates_no_button": "Não",
"onboarding_privacy_updates_title": "Verificar atualizações",
- "onboarding_privacy_updates_whats_sent_button": "Ver exatamente o que é enviado",
+ "onboarding_privacy_updates_whats_sent_button": "Mostrar exatamente o que é enviado",
"onboarding_privacy_updates_yes_button": "Sim",
- "onboarding_privacy_usage_body": "Uma vez por dia, o SurfSense informa a {host} que esta instalação foi usada, com a versão do app, seu sistema, o idioma do app e a semana em que foi instalado. Uma vez por semana, acrescenta contagens em faixas: chats, turnos do agente, fontes, resultados do Studio, chamadas de plugins, se um modelo local ou remoto respondeu e até onde a configuração chegou. O primeiro relatório acrescenta a memória e a placa de vídeo deste computador, em faixas. Nunca documentos, prompts, nomes de arquivos, nomes de modelos, hosts, sua chave de licença ou um ID.",
"onboarding_privacy_usage_no_button": "Não",
"onboarding_privacy_usage_title": "Compartilhar estatísticas de uso",
- "onboarding_privacy_usage_whats_sent_button": "Ver exatamente o que é enviado",
+ "onboarding_privacy_usage_whats_sent_button": "Mostrar exatamente o que é enviado",
"onboarding_privacy_usage_yes_button": "Sim",
"onboarding_progress_aria": "Etapa {step, number} de {total, number} da configuração inicial",
"onboarding_server_option_aria": "Usar um servidor",
@@ -1144,7 +1135,14 @@
"privacy_choices_update_user_agent_header_body": "Sempre SurfSense, sem versão",
"privacy_choices_update_version_header_body": "A versão deste app, como 2.2.4",
"privacy_choices_updates_channel_locked_status": "As atualizações vêm {channel, select, scoop {do Scoop} aur {do AUR} snap {da Snap Store} flatpak {do Flatpak} nix {do Nix} apt {do seu repositório apt} other {do seu gerenciador de pacotes}}, então o SurfSense não as procura por conta própria.",
+ "privacy_choices_updates_details_aria": "O que uma verificação de atualizações envia",
+ "privacy_choices_updates_details_body": "Uma vez por dia, o SurfSense pergunta a {host} se existe uma versão mais nova, enviando seu endereço IP, o nome e a versão do app, seu sistema operacional e a arquitetura dele, e nenhum ID. Se {host} não responder, ele pergunta ao github.com enviando só seu endereço IP e o nome do app. Uma versão mais nova é baixada sozinha do github.com e do githubusercontent.com, que também veem seu endereço IP, e instalada quando você reinicia ou na próxima vez que fecha o SurfSense.",
+ "privacy_choices_updates_mirror_details_body": "Uma vez por dia, o SurfSense pergunta a {host}, o servidor de atualizações definido pela sua organização, se existe uma versão mais nova, enviando seu endereço IP, o nome e a versão do app, seu sistema operacional e a arquitetura dele, e nenhum ID. Uma versão mais nova é baixada sozinha de {host} e instalada quando você reinicia ou na próxima vez que fecha o SurfSense.",
+ "privacy_choices_updates_mirror_summary_body": "Consulta o servidor de atualizações da sua organização uma vez por dia. Nenhum ID é enviado.",
"privacy_choices_updates_policy_locked_status": "A política da sua organização desativou a verificação de atualizações neste computador.",
+ "privacy_choices_updates_summary_body": "Procura uma versão mais nova uma vez por dia. Nenhum ID é enviado.",
+ "privacy_choices_usage_details_aria": "O que as estatísticas de uso enviam",
+ "privacy_choices_usage_details_body": "Uma vez por dia, o SurfSense informa a {host} que esta instalação foi usada, com a versão do app, seu sistema, o idioma do app e a semana em que foi instalado. Uma vez por semana, acrescenta contagens em faixas: chats, turnos do agente, fontes, resultados do Studio, chamadas de plugins, se um modelo local ou remoto respondeu e até onde a configuração chegou. O primeiro relatório acrescenta a memória e a placa de vídeo deste computador, em faixas. Nunca documentos, prompts, nomes de arquivos, nomes de modelos, hosts, sua chave de licença ou um ID.",
"privacy_choices_usage_do_not_track_locked_status": "Desativadas porque DO_NOT_TRACK ou SURFSENSE_NO_TELEMETRY está definido neste computador.",
"privacy_choices_usage_last_sent_aria": "O último relatório de uso enviado",
"privacy_choices_usage_last_sent_label": "Enviado {time}",
@@ -1156,9 +1154,9 @@
"privacy_choices_usage_policy_locked_status": "A política da sua organização desativou as estatísticas de uso neste computador.",
"privacy_choices_usage_report_body": "No máximo um relatório por dia vai para {host}, como o JSON abaixo, sem cookies. Cada valor é uma faixa ou uma palavra fixa. Como toda solicitação, ele leva seu endereço IP, que os logs do provedor de hospedagem guardam por pouco tempo; o SurfSense registra só o país deduzido dele.",
"privacy_choices_usage_report_off_body": "As estatísticas de uso estão desativadas, então nada é enviado. Este seria o próximo relatório se você as ativasse.",
- "privacy_choices_usage_settings_body": "Uma vez por dia, informar a {host} que esta instalação foi usada e, uma vez por semana, acrescentar contagens em faixas, como quantos chats e fontes. Nunca documentos, prompts, nomes de arquivos, nomes de modelos, hosts, sua chave de licença ou um ID.",
"privacy_choices_usage_settings_title": "Estatísticas de uso",
"privacy_choices_usage_settings_whats_sent_button": "Mostrar exatamente o que é enviado",
+ "privacy_choices_usage_summary_body": "Uma contagem diária, em faixas, de como o SurfSense é usado. Nunca seus documentos, prompts ou um ID.",
"privacy_choices_whats_sent_updates_title": "O que uma verificação de atualizações envia",
"privacy_choices_whats_sent_usage_title": "O que as estatísticas de uso enviam",
"resources_breakdown_body": "O que cada parte do SurfSense está usando agora.",
@@ -1671,19 +1669,24 @@
"studio_xlsx_viewer_tabs_aria": "Planilhas",
"studio_xlsx_viewer_truncated_body": "{count, plural, one {Mostrando # linha.} many {Mostrando as primeiras # linhas.} other {Mostrando as primeiras # linhas.}} Baixe o arquivo para ver a pasta de trabalho completa.",
"studio_xlsx_viewer_unreadable_error": "Não foi possível abrir esta pasta de trabalho. Baixe-a para abrir.",
- "updates_settings_body": "Atualizações gratuitas. Depois que você permitir a verificação de atualizações, o SurfSense pergunta a {host} uma vez por dia, e ao github.com quando {host} não responder.",
+ "updates_menu_check_failed_toast": "Não foi possível verificar atualizações",
+ "updates_menu_up_to_date_toast": "O SurfSense está atualizado",
"updates_settings_check_button": "Verificar agora",
"updates_settings_check_error": "Não foi possível verificar atualizações: {message}",
"updates_settings_checking_status": "Verificando…",
"updates_settings_downloading_status": "Baixando {version}…",
"updates_settings_github_body": "Atualizações gratuitas pelo GitHub. Depois que você permitir a verificação de atualizações, o SurfSense também verifica sempre que for iniciado.",
- "updates_settings_mirror_body": "Atualizações gratuitas de {host}, o servidor de atualizações definido pela sua organização. Depois que você permitir a verificação de atualizações, o SurfSense pergunta a ele uma vez por dia.",
"updates_settings_ready_status": "O SurfSense {version} está pronto. Ele é instalado quando você reinicia ou na próxima vez que fecha o SurfSense.",
"updates_settings_restart_button": "Reiniciar para atualizar",
"updates_settings_title": "Atualizações do app",
"updates_settings_up_to_date_status": "O SurfSense está atualizado",
"updates_settings_whats_sent_button": "Mostrar exatamente o que é enviado",
- "updates_title_bar_restart_tooltip": "Reiniciar para instalar {version}",
+ "updates_title_bar_downloading_aria": "Baixando o SurfSense {version}",
+ "updates_title_bar_downloading_label": "Baixando {percent, number, ::percent}",
+ "updates_title_bar_downloading_unknown_label": "Baixando…",
+ "updates_title_bar_ready_label": "Atualizar",
+ "updates_title_bar_restart_aria": "Reiniciar para instalar {version}",
+ "updates_title_bar_restarting_aria": "Reiniciando para instalar {version}",
"workspaces_create_dialog_body": "Mantenha uma biblioteca de fontes e um conjunto de chats separados.",
"workspaces_create_dialog_submit_button": "Criar",
"workspaces_create_dialog_title": "Criar workspace",
diff --git a/surfsense_local/frontend/translations/ru.json b/surfsense_local/frontend/translations/ru.json
index e64b3f0a42..a7d91be6bf 100644
--- a/surfsense_local/frontend/translations/ru.json
+++ b/surfsense_local/frontend/translations/ru.json
@@ -242,12 +242,6 @@
"dashboard_footer_license_clock_status": "Часы сбились",
"dashboard_footer_license_expired_status": "Лицензия истекла",
"dashboard_footer_license_none_status": "Нет лицензии",
- "dashboard_footer_update_check_button": "Проверить обновления",
- "dashboard_footer_update_checking_status": "Проверка…",
- "dashboard_footer_update_current_status": "Актуальная версия",
- "dashboard_footer_update_downloading_status": "Скачивание…",
- "dashboard_footer_update_failed_status": "Не удалось проверить обновления",
- "dashboard_footer_update_restart_button": "Перезапустить для обновления",
"dashboard_right_panel_aria": "Артефакты рабочей области",
"dashboard_right_panel_hide_aria": "Скрыть правую панель",
"dashboard_right_panel_hide_tooltip": "Скрыть правую панель",
@@ -837,16 +831,13 @@
"onboarding_privacy_save_error": "Не удалось сохранить выбор. Попробуйте ещё раз.",
"onboarding_privacy_title": "Настройки конфиденциальности",
"onboarding_privacy_unanswered_status": "Чтобы продолжить, ответьте на оба вопроса.",
- "onboarding_privacy_updates_body": "Раз в день SurfSense спрашивает у {host}, есть ли более новая версия, и передаёт ваш IP-адрес, название и версию приложения, вашу операционную систему и её архитектуру — но никакого идентификатора. Если {host} не отвечает, SurfSense обращается к github.com и передаёт только ваш IP-адрес и название приложения. Новая версия скачивается сама с github.com и githubusercontent.com, которые тоже видят ваш IP-адрес, и устанавливается, когда вы перезапускаете приложение или в следующий раз закрываете его.",
- "onboarding_privacy_updates_mirror_body": "Раз в день SurfSense спрашивает у {host} — сервера обновлений, заданного вашей организацией, — есть ли более новая версия, и передаёт ваш IP-адрес, название и версию приложения, вашу операционную систему и её архитектуру — но никакого идентификатора. Новая версия скачивается сама с {host} и устанавливается, когда вы перезапускаете приложение или в следующий раз закрываете его.",
"onboarding_privacy_updates_no_button": "Нет",
"onboarding_privacy_updates_title": "Проверять обновления",
- "onboarding_privacy_updates_whats_sent_button": "Посмотреть, что именно отправляется",
+ "onboarding_privacy_updates_whats_sent_button": "Показать, что именно отправляется",
"onboarding_privacy_updates_yes_button": "Да",
- "onboarding_privacy_usage_body": "Раз в день SurfSense сообщает {host}, что эта установка использовалась, вместе с версией приложения, вашей системой, языком приложения и неделей установки. Раз в неделю к этому добавляются числа в диапазонах: чаты, шаги агента, источники, результаты Studio, вызовы плагинов, отвечала ли локальная или удалённая модель и насколько продвинулась настройка. Первый отчёт также содержит объём памяти и видеокарту этого компьютера в диапазонах. Никогда не отправляются документы, промпты, имена файлов, названия моделей, хосты, ваш лицензионный ключ или идентификатор.",
"onboarding_privacy_usage_no_button": "Нет",
"onboarding_privacy_usage_title": "Отправлять статистику использования",
- "onboarding_privacy_usage_whats_sent_button": "Посмотреть, что именно отправляется",
+ "onboarding_privacy_usage_whats_sent_button": "Показать, что именно отправляется",
"onboarding_privacy_usage_yes_button": "Да",
"onboarding_progress_aria": "Шаг настройки {step, number} из {total, number}",
"onboarding_server_option_aria": "Использовать сервер",
@@ -1144,7 +1135,14 @@
"privacy_choices_update_user_agent_header_body": "Всегда SurfSense, без версии",
"privacy_choices_update_version_header_body": "Версия этого приложения, например 2.2.4",
"privacy_choices_updates_channel_locked_status": "Обновления приходят {channel, select, scoop {через Scoop} aur {из AUR} snap {из Snap Store} flatpak {через Flatpak} nix {через Nix} apt {из вашего репозитория apt} other {через ваш менеджер пакетов}}, поэтому SurfSense сам их не проверяет.",
+ "privacy_choices_updates_details_aria": "Что отправляет проверка обновлений",
+ "privacy_choices_updates_details_body": "Раз в день SurfSense спрашивает у {host}, есть ли более новая версия, и передаёт ваш IP-адрес, название и версию приложения, вашу операционную систему и её архитектуру — но никакого идентификатора. Если {host} не отвечает, SurfSense обращается к github.com и передаёт только ваш IP-адрес и название приложения. Новая версия скачивается сама с github.com и githubusercontent.com, которые тоже видят ваш IP-адрес, и устанавливается, когда вы перезапускаете приложение или в следующий раз закрываете его.",
+ "privacy_choices_updates_mirror_details_body": "Раз в день SurfSense спрашивает у {host} — сервера обновлений, заданного вашей организацией, — есть ли более новая версия, и передаёт ваш IP-адрес, название и версию приложения, вашу операционную систему и её архитектуру — но никакого идентификатора. Новая версия скачивается сама с {host} и устанавливается, когда вы перезапускаете приложение или в следующий раз закрываете его.",
+ "privacy_choices_updates_mirror_summary_body": "Раз в день обращается к серверу обновлений вашей организации. Без идентификатора.",
"privacy_choices_updates_policy_locked_status": "Политика вашей организации отключила проверку обновлений на этом компьютере.",
+ "privacy_choices_updates_summary_body": "Раз в день проверяет, есть ли более новая версия. Без идентификатора.",
+ "privacy_choices_usage_details_aria": "Что отправляет статистика использования",
+ "privacy_choices_usage_details_body": "Раз в день SurfSense сообщает {host}, что эта установка использовалась, вместе с версией приложения, вашей системой, языком приложения и неделей установки. Раз в неделю к этому добавляются числа в диапазонах: чаты, шаги агента, источники, результаты Studio, вызовы плагинов, отвечала ли локальная или удалённая модель и насколько продвинулась настройка. Первый отчёт также содержит объём памяти и видеокарту этого компьютера в диапазонах. Никогда не отправляются документы, промпты, имена файлов, названия моделей, хосты, ваш лицензионный ключ или идентификатор.",
"privacy_choices_usage_do_not_track_locked_status": "Отключено, потому что на этом компьютере задана переменная DO_NOT_TRACK или SURFSENSE_NO_TELEMETRY.",
"privacy_choices_usage_last_sent_aria": "Последний отправленный отчёт об использовании",
"privacy_choices_usage_last_sent_label": "Отправлено {time}",
@@ -1156,9 +1154,9 @@
"privacy_choices_usage_policy_locked_status": "Политика вашей организации отключила статистику использования на этом компьютере.",
"privacy_choices_usage_report_body": "На {host} уходит не больше одного отчёта в день — в виде JSON ниже, без cookie. Каждое значение — это диапазон или фиксированное слово. Как и любой запрос, отчёт передаёт ваш IP-адрес, который недолго хранится в журналах хостинг-провайдера; SurfSense записывает только страну, определённую по нему.",
"privacy_choices_usage_report_off_body": "Статистика использования отключена, поэтому ничего не отправляется. Так выглядел бы следующий отчёт, если бы вы её включили.",
- "privacy_choices_usage_settings_body": "Раз в день сообщать {host}, что эта установка использовалась, а раз в неделю добавлять числа в диапазонах, например сколько чатов и источников. Никогда не отправляются документы, промпты, имена файлов, названия моделей, хосты, ваш лицензионный ключ или идентификатор.",
"privacy_choices_usage_settings_title": "Статистика использования",
"privacy_choices_usage_settings_whats_sent_button": "Показать, что именно отправляется",
+ "privacy_choices_usage_summary_body": "Ежедневные числа в диапазонах о том, как используется SurfSense. Никогда не отправляются ваши документы, промпты или идентификатор.",
"privacy_choices_whats_sent_updates_title": "Что отправляет проверка обновлений",
"privacy_choices_whats_sent_usage_title": "Что отправляет статистика использования",
"resources_breakdown_body": "Что сейчас использует каждая часть SurfSense.",
@@ -1671,19 +1669,24 @@
"studio_xlsx_viewer_tabs_aria": "Листы",
"studio_xlsx_viewer_truncated_body": "{count, plural, one {Показана # строка} few {Показаны первые # строки} many {Показаны первые # строк} other {Показаны первые # строки}}. Скачайте файл, чтобы увидеть книгу целиком.",
"studio_xlsx_viewer_unreadable_error": "Не удалось открыть эту книгу. Скачайте её, чтобы открыть.",
- "updates_settings_body": "Бесплатные обновления. Когда вы разрешите проверку обновлений, SurfSense будет раз в день обращаться к {host}, а если {host} не отвечает — к github.com.",
+ "updates_menu_check_failed_toast": "Не удалось проверить обновления",
+ "updates_menu_up_to_date_toast": "SurfSense обновлён до последней версии",
"updates_settings_check_button": "Проверить сейчас",
"updates_settings_check_error": "Не удалось проверить обновления: {message}",
"updates_settings_checking_status": "Проверка…",
"updates_settings_downloading_status": "Скачивание {version}…",
"updates_settings_github_body": "Бесплатные обновления через GitHub. Когда вы разрешите проверку обновлений, SurfSense будет проверять их и при каждом запуске.",
- "updates_settings_mirror_body": "Бесплатные обновления с {host} — сервера обновлений, заданного вашей организацией. Когда вы разрешите проверку обновлений, SurfSense будет обращаться к нему раз в день.",
"updates_settings_ready_status": "SurfSense {version} готов. Обновление установится при перезапуске или при следующем закрытии приложения.",
"updates_settings_restart_button": "Перезапустить для обновления",
"updates_settings_title": "Обновления приложения",
"updates_settings_up_to_date_status": "SurfSense обновлён до последней версии",
"updates_settings_whats_sent_button": "Показать, что именно отправляется",
- "updates_title_bar_restart_tooltip": "Перезапустить, чтобы установить {version}",
+ "updates_title_bar_downloading_aria": "Скачивание SurfSense {version}",
+ "updates_title_bar_downloading_label": "Скачивание {percent, number, ::percent}",
+ "updates_title_bar_downloading_unknown_label": "Скачивание…",
+ "updates_title_bar_ready_label": "Обновить",
+ "updates_title_bar_restart_aria": "Перезапустить, чтобы установить {version}",
+ "updates_title_bar_restarting_aria": "Перезапуск для установки {version}",
"workspaces_create_dialog_body": "Отдельная библиотека источников и свой набор чатов.",
"workspaces_create_dialog_submit_button": "Создать",
"workspaces_create_dialog_title": "Создать рабочую область",
diff --git a/surfsense_local/frontend/translations/zh-CN.json b/surfsense_local/frontend/translations/zh-CN.json
index 28148fbb07..bf87a37ee5 100644
--- a/surfsense_local/frontend/translations/zh-CN.json
+++ b/surfsense_local/frontend/translations/zh-CN.json
@@ -242,12 +242,6 @@
"dashboard_footer_license_clock_status": "时钟不准",
"dashboard_footer_license_expired_status": "许可证已过期",
"dashboard_footer_license_none_status": "无许可证",
- "dashboard_footer_update_check_button": "检查更新",
- "dashboard_footer_update_checking_status": "检查中…",
- "dashboard_footer_update_current_status": "已是最新",
- "dashboard_footer_update_downloading_status": "下载中…",
- "dashboard_footer_update_failed_status": "更新检查失败",
- "dashboard_footer_update_restart_button": "重启以更新",
"dashboard_right_panel_aria": "工作区产物",
"dashboard_right_panel_hide_aria": "隐藏右侧面板",
"dashboard_right_panel_hide_tooltip": "隐藏右侧面板",
@@ -837,13 +831,10 @@
"onboarding_privacy_save_error": "无法保存你的选择。请重试。",
"onboarding_privacy_title": "隐私选择",
"onboarding_privacy_unanswered_status": "两项都回答后才能继续。",
- "onboarding_privacy_updates_body": "SurfSense 每天向 {host} 询问一次是否有新版本,发送你的 IP 地址、应用名称和版本、你的操作系统及其架构,不发送 ID。{host} 无法响应时,它会询问 github.com,只发送你的 IP 地址和应用名称。新版本会自动从 github.com 和 githubusercontent.com 下载,它们同样能看到你的 IP 地址,并在你重启或下次退出时安装。",
- "onboarding_privacy_updates_mirror_body": "SurfSense 每天向你的组织设置的更新服务器 {host} 询问一次是否有新版本,发送你的 IP 地址、应用名称和版本、你的操作系统及其架构,不发送 ID。新版本会自动从 {host} 下载,并在你重启或下次退出时安装。",
"onboarding_privacy_updates_no_button": "否",
"onboarding_privacy_updates_title": "检查更新",
"onboarding_privacy_updates_whats_sent_button": "查看具体发送的内容",
"onboarding_privacy_updates_yes_button": "是",
- "onboarding_privacy_usage_body": "SurfSense 每天告诉 {host} 一次此安装已被使用,同时附上应用版本、你的系统、应用语言和安装所在的周。每周一次,它会加上按区间统计的数量:对话、智能体轮次、来源、Studio 产出、插件调用、回答的是本地模型还是远程模型,以及设置进行到哪一步。第一份报告还会按区间附上这台电脑的内存和显卡。绝不发送文档、提示词、文件名、模型名称、主机、你的许可证密钥或 ID。",
"onboarding_privacy_usage_no_button": "否",
"onboarding_privacy_usage_title": "共享使用统计",
"onboarding_privacy_usage_whats_sent_button": "查看具体发送的内容",
@@ -1144,7 +1135,14 @@
"privacy_choices_update_user_agent_header_body": "始终为 SurfSense,不含版本",
"privacy_choices_update_version_header_body": "此应用的版本,例如 2.2.4",
"privacy_choices_updates_channel_locked_status": "更新由{channel, select, scoop { Scoop } aur { AUR } snap { Snap Store } flatpak { Flatpak } nix { Nix } apt {你的 apt 软件源} other {你的软件包管理器}}提供,因此 SurfSense 不会自行检查更新。",
+ "privacy_choices_updates_details_aria": "检查更新会发送什么",
+ "privacy_choices_updates_details_body": "SurfSense 每天向 {host} 询问一次是否有新版本,发送你的 IP 地址、应用名称和版本、你的操作系统及其架构,不发送 ID。{host} 无法响应时,它会询问 github.com,只发送你的 IP 地址和应用名称。新版本会自动从 github.com 和 githubusercontent.com 下载,它们同样能看到你的 IP 地址,并在你重启或下次退出时安装。",
+ "privacy_choices_updates_mirror_details_body": "SurfSense 每天向你的组织设置的更新服务器 {host} 询问一次是否有新版本,发送你的 IP 地址、应用名称和版本、你的操作系统及其架构,不发送 ID。新版本会自动从 {host} 下载,并在你重启或下次退出时安装。",
+ "privacy_choices_updates_mirror_summary_body": "每天向你的组织的更新服务器检查一次。不发送 ID。",
"privacy_choices_updates_policy_locked_status": "你的组织策略已在这台电脑上关闭更新检查。",
+ "privacy_choices_updates_summary_body": "每天检查一次是否有新版本。不发送 ID。",
+ "privacy_choices_usage_details_aria": "使用统计会发送什么",
+ "privacy_choices_usage_details_body": "SurfSense 每天告诉 {host} 一次此安装已被使用,同时附上应用版本、你的系统、应用语言和安装所在的周。每周一次,它会加上按区间统计的数量:对话、智能体轮次、来源、Studio 产出、插件调用、回答的是本地模型还是远程模型,以及设置进行到哪一步。第一份报告还会按区间附上这台电脑的内存和显卡。绝不发送文档、提示词、文件名、模型名称、主机、你的许可证密钥或 ID。",
"privacy_choices_usage_do_not_track_locked_status": "已关闭,因为这台电脑设置了 DO_NOT_TRACK 或 SURFSENSE_NO_TELEMETRY。",
"privacy_choices_usage_last_sent_aria": "最近一次发送的使用报告",
"privacy_choices_usage_last_sent_label": "发送于 {time}",
@@ -1156,9 +1154,9 @@
"privacy_choices_usage_policy_locked_status": "你的组织策略已在这台电脑上关闭使用统计。",
"privacy_choices_usage_report_body": "每天最多向 {host} 发送一份报告,内容即下方的 JSON,不带 Cookie。每个值都是一个区间或固定词。和所有请求一样,它会带上你的 IP 地址,托管服务商的日志会短暂保留该地址;SurfSense 只记录据此推断出的国家。",
"privacy_choices_usage_report_off_body": "使用统计已关闭,因此不会发送任何内容。如果你开启,下一份报告会是这样。",
- "privacy_choices_usage_settings_body": "每天告诉 {host} 一次此安装已被使用,每周一次加上按区间统计的数量,例如对话和来源的数量。绝不发送文档、提示词、文件名、模型名称、主机、你的许可证密钥或 ID。",
"privacy_choices_usage_settings_title": "使用统计",
"privacy_choices_usage_settings_whats_sent_button": "查看具体发送的内容",
+ "privacy_choices_usage_summary_body": "每天按区间统计 SurfSense 的使用情况。绝不发送你的文档、提示词或 ID。",
"privacy_choices_whats_sent_updates_title": "检查更新会发送什么",
"privacy_choices_whats_sent_usage_title": "使用统计会发送什么",
"resources_breakdown_body": "SurfSense 各部分当前的占用。",
@@ -1671,19 +1669,24 @@
"studio_xlsx_viewer_tabs_aria": "工作表",
"studio_xlsx_viewer_truncated_body": "仅显示前 {count, plural, other {# 行}}。下载文件可查看完整工作簿。",
"studio_xlsx_viewer_unreadable_error": "无法打开这个工作簿。请下载后打开。",
- "updates_settings_body": "免费更新。允许检查更新后,SurfSense 每天向 {host} 询问一次,{host} 无法响应时则询问 github.com。",
+ "updates_menu_check_failed_toast": "无法检查更新",
+ "updates_menu_up_to_date_toast": "SurfSense 已是最新",
"updates_settings_check_button": "立即检查",
"updates_settings_check_error": "无法检查更新:{message}",
"updates_settings_checking_status": "检查中…",
"updates_settings_downloading_status": "正在下载 {version}…",
"updates_settings_github_body": "来自 GitHub 的免费更新。允许检查更新后,SurfSense 每次启动时也会检查。",
- "updates_settings_mirror_body": "从你的组织设置的更新服务器 {host} 获取免费更新。允许检查更新后,SurfSense 每天向它询问一次。",
"updates_settings_ready_status": "SurfSense {version} 已准备就绪,将在你重启或下次退出时安装。",
"updates_settings_restart_button": "重启以更新",
"updates_settings_title": "应用更新",
"updates_settings_up_to_date_status": "SurfSense 已是最新",
"updates_settings_whats_sent_button": "查看具体发送的内容",
- "updates_title_bar_restart_tooltip": "重启以安装 {version}",
+ "updates_title_bar_downloading_aria": "正在下载 SurfSense {version}",
+ "updates_title_bar_downloading_label": "正在下载 {percent, number, ::percent}",
+ "updates_title_bar_downloading_unknown_label": "下载中…",
+ "updates_title_bar_ready_label": "更新",
+ "updates_title_bar_restart_aria": "重启以安装 {version}",
+ "updates_title_bar_restarting_aria": "正在重启以安装 {version}",
"workspaces_create_dialog_body": "单独保存一套来源库和对话。",
"workspaces_create_dialog_submit_button": "创建",
"workspaces_create_dialog_title": "创建工作区",