Skip to content

[Local|Release] Release desktop 2.2.4 - #2351

Merged
AnishSarkar22 merged 331 commits into
mainfrom
dev
Oct 25, 2026
Merged

AnishSarkar22 merged 331 commits into
mainfrom
dev

Conversation

@AnishSarkar22

@AnishSarkar22 AnishSarkar22 commented Oct 25, 2026 •

Copy link
Copy Markdown
Collaborator

What

Releases desktop 2.2.4: dev into main. The changelog entry is #2349 and the version bump #2350.

#2338's package-manager channels ride along but stay off: their secrets are not set, so release-channels.yml skips every channel.

Why

The 2.2.4 release, per surfsense_local/RELEASE.md. The tag goes on this pull request's merge commit.

Fixes #

How to test

Before merging, with Save only (no redeploy):

  • Vercel: Root Directory surfsense_web → www; add DESKTOP_TELEMETRY_POSTHOG_KEY; updates.surfsense.com and usage.surfsense.com attached to the project.
  • Coolify surfsense-plugin-host: base directory /backend, Dockerfile /server/Dockerfile, a volume at /srv/remote/usage, reddit,instagram added to PLUGINS, and the license settings copied from surfprod-fastapi-api.
  • Coolify surfsense-plugin-registry: base directory /backend/plugins/registry.

After merging:

  • Both Coolify deployments succeed; https://plugins.surfsense.com/health answers ok and /plugins.json loads.
  • https://api.surfsense.com/health still answers 200 (the old backend until the cutover).
  • Vercel deploys www from main; https://updates.surfsense.com/api/updates/stable.yml answers 200.
  • Then tag v2.2.4 on the merge commit and follow RELEASE.md from step 5.

High-level PR Summary

This is the 2.2.4 desktop release from dev into main. The repository has been dramatically restructured: the legacy self-hosted stack (surfsense_backend, surfsense_web, surfsense_mcp, docker/) has been moved to a legacy branch, while the desktop app continues on main. A new unified backend in backend/ consolidates the license portal, plugin host, and SurfSense's own plugins. The site has moved from surfsense_web/ to www/, which now serves update feeds and usage routes the desktop calls. Package managers (winget, AUR, Snap Store) can now install and update the desktop app. Release scripts have been added for builds (a minimal OpenCV, channel releases, stats collection), and the workflow infrastructure has been significantly reworked to support this new architecture. The changelog entry is #2349 and the version bump is #2350.

⏱️ Estimated Review Time: 1-3 hours

💡 Review Order Suggestion
Order File Path
1 surfsense_local/RELEASE.md
2 .github/workflows/release-local.yml
3 .github/workflows/release-channels.yml
4 backend/license/portal.py
5 backend/plugins/host/app.py
6 www/features/desktop-telemetry/update-feed/fixtures/stable.yml
7 .github/CODEOWNERS
8 README.md
9 CONTRIBUTING.md
10 AGENTS.md

Need help? Join our Discord

Cedric921 and others added 30 commits October 22, 2026 06:44
… download host

The update prompt and the docs named only the platform's release file. A check
first reads the releases feed on github.com, and GitHub serves release files
from githubusercontent.com, which also sees the IP address. Translated into
every locale.
… own header merge

The header test only checked what attachUpdater assigns. It now also runs the
installed electron-updater's merge with a real-looking ID, so a version that
merged the other way would fail the suite. The comment now says the ID went on
the feed and manifest requests, not downloads.
It cuts http(s) and ws(s) addresses up to the first space. An address without
its scheme, with escaped slashes or with an unencoded space keeps the rest, so
the docs no longer say a page or search never reaches a log.
…license key

The success page already says so; the activation guide's lede still said
only that the file is checked on your own machine.
A check can make a third request, for the fallback manifest, when the
platform's release file is missing.
A report waits up to 5 s on the local API summary. A no given in that window now stops the POST.
…t repeat its prefix

Settings shows "Could not check for updates: {message}". The provider's error now names the hosts and their problems without that prefix.
The summary is the seam with the backend stream: a wrong path or parameter would only show as reports full of unknowns.
The redirect built the GitHub URL from the pinned tag without checking it
exists. Vercel deploys a new pin while its release is still a draft, so for
the hour before publishing every README, grid and licence-mail link 302'd to
a 404, and a never-published `v` did the same.

It now checks the tag against the newest 30 published releases (cached for
an hour) and otherwise redirects to /releases/latest/download/<file>,
recording the version as "latest". The handlers are built from injected
GitHub, PostHog and after() seams, so the tests now prove a browser GET is
counted once after the response and a bot, scanner or HEAD never is.
The day used to be marked before the check ran, so a launch while offline, or with both hosts down, used up the day's check. It is now marked once a manifest was read from either host; until then the hourly tick tries again, three tries a UTC day at most.
The two PyInstaller bundles shared 1,531 identical files that the NSIS
archive packed twice. One bundle now runs as `api` or `worker <queue>`;
the API role hides every module an API-only analysis did not collect, so
it imports exactly what its own binary did.

Leave out what no code path opens: RapidOCR's wheel copy of its models,
onnxruntime's C API library, OpenCV's FFmpeg video plugin on Windows,
torch's protoc and (Linux) C++ tests, every transformers model family
Docling's layout engine does not load, dev tooling, faker/polyfactory,
pywin32's MFC, hf_xet. torch and torchvision ship as bytecode; library
bytecode drops docstrings where nothing reads them. Linux strips symbol
tables, macOS strips local symbols. OpenCV is the headless wheel.

CI stages the parser pack before the frozen checks and adds a Docling
reading compared byte for byte with source, both roles of the binary,
and an import of every native extension.
…ey are personless, not unlinkable

installer_download carries where the link was, not a channel, and Stripe and Keygen records can match a trial or purchase count by time, so the page no longer says the counts cannot be linked to anyone.
The Payment Link collects card details and a billing country too; what the page means is that there is no account to create.
…sts to Individual

The FAQ still said the $60 window was open for 30 days after launch, which ended on 18 Oct 2026, beside the checkout page's unconditional $60. The feature build request is an Individual licence benefit, not every paid licence's.
A modified click followed the href computed at mount, before PostHog had loaded, so Stripe's client_reference_id could differ from the event's channel.
…ies on

The after-payment redirect and a one-time price are set by hand in Stripe; the page's download and no-renewal promises are only true with both.
…ept-Language

Settings shows the last feed request as sent, but it left out the User-Agent the outbound session sets. That session also gets an empty Accept-Language list, so Chromium cannot add one from the system locale to update checks or usage reports.
… models on the device

Agent turns live in their engine, plugin calls leave only a last-call time,
and no reply records which kind of model answered, so the API keeps hourly
counts of those three in usage-counts.json in the data folder. Counts only,
pruned after 40 days, and marked with when counting began so a window that
reaches back before it reads as unknown rather than zero. Nothing is sent.
GET /usage-statistics/summary?since= answers the weekly report's week,
hardware and ui_language in the contract's buckets, computed at read time
from surfsense.db, the on-device counts and the model runtime's device
probe. Values that cannot be derived are unknown.
The docs, pricing, trial and cloud export links in the desktop app now carry
utm_source=desktop, utm_medium=app, utm_content=<the place it was opened
from> and app_version, so the site can count visits from the app by surface.
No ID and nothing else is added; GitHub and Discord links stay plain, and the
external-link allowlist passes surfsense.com with any query.
…tics in Settings

Settings › About gains a Usage statistics switch beside App updates, each with
a "Show exactly what's sent" dialog: the last update request with its URL and
headers (or the only headers a check sends, before the first one), and the
next usage report built but not sent, with the last one sent and when.
Settings › Network names where checks go (updates.surfsense.com, falling back
to github.com, or a policy's mirror) and lists usage statistics as a row. A
setting an admin policy or DO_NOT_TRACK forces is held off and says why, and
Check now is disabled while a policy turns updates off.

The update consent prompt now says what a check sends: the IP address, the
app's version, OS and architecture, no ID, with github.com behind our host.
window.surfsense.privacy is declared as Electron main exposes it (contract
05, part C). Translated into every locale; "usage statistics" joins the
glossary.
…tistics

Right after the welcome, a Privacy choices step asks two separate questions:
whether to check for updates (once a day from updates.surfsense.com, falling
back to github.com; the app's version, OS and architecture, no ID) and
whether to share usage statistics (a daily active mark and weekly counts in
ranges; never documents, prompts, file names, model names, hosts, the license
key or an ID). Neither is preselected, the Yes and No buttons weigh the same,
and Continue waits for both answers before calling completeFirstRun. Each row
opens exactly what it sends. A row a policy or DO_NOT_TRACK holds off is
answered already and says why.

Only a new install sees it: an install that finished onboarding never reaches
this screen, and one that already answered (firstRunAsked) skips it, as does
the app outside Electron.
…stics

ADR 0068 lets the app send a daily update check through updates.surfsense.com
(version, platform and OS, no ID, GitHub behind it) and, only with a separate
yes, a usage report in ranges to usage.surfsense.com. It supersedes ADR 0016
and amends 0017, 0020 and 0060, whose status lines now say so.

Contract 5 holds the shared contract for Electron main, the app's UI and API,
and the two website routes, with the details the first build settled.
usage-statistics.md describes what is sent, when, consent, the admin policy,
DO_NOT_TRACK, the local summary endpoint and its tests; updates.md, egress.md,
about.md and overview.md now say what a check sends and where, and that the
app's surfsense.com links say where a visit came from.
…s send

Section 3.2 lists updates.surfsense.com, falling back to github.com, with the
version, platform and OS a check sends, and usage.surfsense.com with every
field of the opt-in report. Section 3.3 replaces "no telemetry" and "we cannot
tell whether you ever opened it" with what SurfSense can count when each is
on, that neither carries an ID, and that the host uses the IP address only to
route the request and derive a country. Section 5 names consent as the basis
for usage statistics, withdrawn in Settings, with DO_NOT_TRACK=1 or by an
administrator's policy, and the update service the user enabled for update
checks. The meta description and the short version say the same.
…nd SPA

Updates download only the changed blocks again: electron-updater's GitHub
provider sends single ranges, which GitHub serves. A patch makes a range
answered with the whole file fall back to a full download instead of
writing the file once per range. Windows drops a stale current.blockmap
before each check, and pending/ is emptied once its version is installed
or withdrawn; installer.exe stays as the next update's base.

eSpeak data keeps the 21 dictionaries a curated voice can reach, checked
at staging by reading every kept dictionary's switches, with a dated note
in the GPL notice. The dmg is LZFSE, smaller and faster to open than
zlib. The SPA ships only the two Shiki themes it uses, no Oniguruma wasm,
and only the woff2 of each font. An installer-size budget fails a build
that grows past it.

All Chromium locales stay: dropping a .pak changes Intl's default locale
and Chromium's own text for that OS language.
…rote

PyInstaller's strip also stripped numpy's vendored OpenBLAS, which then
failed to load ("ELF load command address/offset not page-aligned") on
the first dry run. Strip in build_binaries.py instead, skipping
<package>.libs and hashed names. The API role test gives the first
catalog read, which probes the hardware in a child, room on a CI runner.
A Basic chat turn and an Agentic turn both marked the install as answered
before the model replied, so a first turn that failed read as setup's first
answer. Count the kind of model when a reply ends with text, count an
Agentic turn once its engine took it, and drive both through the routes in
tests that run without the bundled model.
… agent

The update prompt had lost the download host, githubusercontent.com, and
the first-run row, now the main consent, named neither the IP address nor
the download. Both now say where a newer version comes from and that it
installs on quit. The headers dialog lists the User-Agent the outbound
session sets, the GitHub fallback says it sends it, and the usage report
says the hosting provider's logs keep the address briefly.

Without the privacy bridge Electron main still reads GitHub's releases,
so the update prompt and Settings keep the GitHub wording there instead
of naming updates.surfsense.com.
AnishSarkar22 and others added 24 commits October 25, 2026 03:53
The redirect tests match with Next's own getPathMatch and
prepareDestination, typed and called with the options its server uses,
instead of its untyped bundled path-to-regexp. The blog post's MDX body
takes MDXComponents. `pnpm typecheck` generates the MDX and route types
first, so the new tsc-www hook works on a fresh clone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…folders

refactor(www): colocate route code and rename website/ to www/
The site is now a pill under the text, as in the chat composer, with each
site's own logo from the plugin icons. The dialog keeps a fixed height and
scrolls its results beneath the box; before the first search it offers
every site, one click each.
Separators between result rows, Show more as the last row of the scroll
instead of a pinned bar, a straight arrow on the send button, an icon on
the start screen, and no text selection in the dialog or its site menu.
…sage

The 60-character cut left most titles ending mid-word in the chats
dialog. A thread's name now runs as long as a chat's own title may be.
Long text scrolled under the new-chat composer's controls, which sat over
the box, and stopped short of the top border in both composers. The
controls are now a row under the text, and the text scrolls up to the
border.
feat(sources): rework the add-from-web dialog around one search box
A second demo shows Add from the web followed by an HTML web page. Each
video gets a bold caption above it, translated in every README, since a
caption below one would read as the title of the next.
docs(readme): add the web-to-web-page demo and caption both videos
…hoices

Privacy choices is now onboarding's first step, counted in the progress bar
and shown every time onboarding runs, with earlier answers chosen. Back from
the first model step returns to it.

Each choice is one line, with the full description and every host behind an
info icon and a link to the exact request or report. Yes and No are one
control whose chosen half is filled. Settings › About uses the same text and
info icon.
…UDA variant

The Docker proposal moves the supervisor and runtime staging out of
electron/ into sidecars/ and runtimes/, adds the headless entry in
headless/, and puts the image in docker/surfsense/ built from the repo
root. latest moves when the GitHub release is published. The GPU image is
-cuda on CUDA 12.8, with -cuda13 later for arm64, and each runtime keeps
one source shared with the desktop. ADR 0072 and 0073 record the layout
and the variants; 0073 supersedes 0035's CUDA variant names.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Docker proposal maps the container's networks and says why Caddy is
the one listener. The GPU proposal closes its driver question: -cuda
needs 570.26 on Linux and 570.65 on Windows, kept beside the llama.cpp
pin and enforced by the installer, NVIDIA_REQUIRE_CUDA and the GPU check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user installs Claude Code in the container and signs in there; HOME
on /data keeps both across updates. The Claude dialog shows the container's
commands when the API reports it runs in Docker. Closes the proposal's
question about the claude CLI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every OAuth callback returns to the origin the user's browser is on.
Plugins, Notion included, try a CIMD client, then DCR, then the user's
own app, then paste-back; ChatGPT follows the rule as far as OpenAI's
client allows. Access from another machine takes HTTPS. Closes the
proposal's last open question.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tle bar

While an update downloads, a ring around a pill beside the right-panel
toggle fills with the percentage electron-updater reports, sent once per
whole percent. Once ready, the pill fills and restarts into the update on
click. On hover or focus it widens to show "Downloading 64%" or "Update".

The sidebar no longer shows updates. Check for Updates… in the macOS menu
reports up to date or a failed check in a toast, since Settings may be
closed.
docs(docker): one core with two entries, one CUDA variant, and sign-ins for the Docker image
feat(desktop): privacy choices as an onboarding step, and updates in the title bar
@vercel

vercel Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
surf-sense-frontend Ready Ready Preview Oct 25, 2026 10:13pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Oct 25, 2026

Copy link
Copy Markdown

Walkthrough

Warning

Review details and warnings were omitted to fit the comment limit.

@AnishSarkar22
AnishSarkar22 merged commit c2231c5 into main Oct 25, 2026
43 of 46 checks passed

This branch was successfully deployed

1 active deployment
Preview — 9f076312 Deployed Oct 25, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants