conduit/lib/features
cogwheel c149c6e686
feat: gate app on unsupported Open WebUI server versions (#545)
* feat: gate app on unsupported Open WebUI server versions

Refuse to operate against Open WebUI servers newer than this build is
known to support (max 0.10.1) and surface a clear "downgrade the server
or wait for an app update" message, instead of failing in confusing ways
deep inside features when the upstream API drifts.

Enforced at two points:
- Connect time (direct + reverse-proxy flows): probe /api/config and, if
  the version is unsupported, show a blocking dialog and abort without
  saving the server.
- Runtime/restored sessions: a router redirect gate gates every in-app
  route to a blocking ServerIncompatiblePage while the active server is
  incompatible (covers servers upgraded after connecting). The
  server-connection route stays reachable so users can switch servers,
  and the gate auto-clears once a supported version is reported.

Details:
- ServerVersionCompat: pure, unit-tested comparator; tolerates a "v"
  prefix and -dev/+build suffixes; fails open on unknown/unparseable
  versions to avoid false lockouts.
- BackendConfig.version captured from /api/config and round-tripped
  through the local cache.
- serverIncompatibleProvider derives the gate state; RouterNotifier
  listens to it.
- Reviewer/demo mode bypasses the gate.
- New strings localized across all 12 supported locales.

To support a newer server later, bump ServerVersionCompat.maxSupportedVersion
after validating against openwebui-src/.

* fix: address PR review — don't trap users on the compatibility gate

Resolves the P1 flagged independently by Macroscope and Greptile, plus a
CodeRabbit maintainability nit.

- BackendConfigNotifier: the cached backend config (and its version) is
  global, not per-server. On a genuine active-server switch (A->B), drop
  the stale config (fail open) and re-fetch against the new server, so a
  previous server's version can't keep the gate up. Tracks the last
  non-null server id so the switch is detected even when activeServer
  passes through a transient loading/null state on invalidate.
- Router gate: allow the full auth/connection flow through the gate (not
  just server-connection) so the "use a different server" recovery can
  actually reach the sign-in page while the old, unsupported server is
  still active. The connect-time gate still prevents authenticating into
  an unsupported server.
- server_connection_page: extract the duplicated version-compat refusal
  into a single _refuseIfServerIncompatible helper shared by the direct
  and reverse-proxy flows; drop the redundant _isConnecting reset (the
  caller's finally handles it).

Greptile's P2 (corrupted zh_Hant title) was a false positive — verified
no U+FFFD replacement characters in any ARB file.

* fix: make the compatibility gate server-aware (PR review round 2)

The previous fix used a fragile active-server-change listener that the
re-review showed was racy and incomplete (Macroscope High, CodeRabbit
Major, Greptile P1). Replace it with server-id tagging, which is robust
against every stale-config path:

- BackendConfig gains a serverId, set in _loadBackendConfig to the active
  server it was fetched from and persisted in the cache.
- serverIncompatibleProvider now watches activeServerProvider and gates
  only when the cached config's serverId matches the active server id;
  otherwise it fails open. A stale config from a previously-active server
  — left over after a switch, an out-of-order refresh, or restored from
  disk on a cold start — can no longer trap a supported server on the
  gate. Removed the listener and its saveLocalBackendConfig(null) race.

- Router gate (Macroscope Medium): narrow the exemption so an in-progress
  connection/auth flow is allowed through the gate only when it targets a
  DIFFERENT server than the active (unsupported) one. Re-authenticating
  into the same unsupported server now stays gated instead of being
  waved through on /authentication, /sso-auth, etc.

Adds server_incompatible_provider_test.dart covering matching/mismatched/
untagged/absent configs.

* fix: gate legacy untagged caches + canonicalize recovery URLs (review r3)

- serverIncompatibleProvider (Macroscope Medium): a backend config restored
  from a pre-tagging app version has a null serverId. Failing open on null
  let an unsupported server through on cold start. Treat a null serverId as
  the active server's config so it still gates; only an explicitly
  different (non-null) serverId fails open. Fresh configs are always tagged,
  so this can't reintroduce the server-switch trap.
- _isConnectFlowToDifferentServer (CodeRabbit Minor): canonicalize URLs
  (trim, strip trailing slash, lowercase) before comparing, so the same
  server entered/stored with a trailing slash or different case isn't read
  as a different server and granted the gate exemption.
- Tests updated: a null-serverId (legacy) config now gates an unsupported
  active server and stays open for a supported one.

Macroscope's "serverConnection loops back to the gate" High is a false
positive: the gate returns null for Routes.serverConnection before the
authenticated-user redirect at lines 142-147, so that branch is
unreachable while incompatible.

* fix: gate only on a config confirmed for the active server (review r4)

Resolves Greptile P1: a legacy untagged cache (null serverId) could trap a
supported server on the incompatibility gate when a refresh was pending or
failed after switching away from an unsupported server.

This is the deliberate counter-decision to the earlier Macroscope Medium
(which wanted null serverId to gate). The two concerns conflict only for an
unattributable legacy cache, and failing open is the safer side:

- gate only when config.serverId == active server id (a config the refresh
  confirmed came from this server);
- a different non-null serverId is stale-after-switch -> fail open;
- a null serverId is a pre-tagging cache we can't attribute -> fail open.

The cost is that right after upgrading the app while on an unsupported
server, the gate stays open until the build()-time refresh returns a
freshly-tagged config (~one round-trip). That brief, self-healing delay is
preferable to risking a false lockout from a valid server. Documented inline
so this doesn't oscillate.

Test updated: an untagged legacy config now fails open.
2026-06-30 23:48:19 +05:30
..
auth feat: gate app on unsupported Open WebUI server versions (#545) 2026-06-30 23:48:19 +05:30
channels feat(notifications): in-app notification parity layer from socket events (#526) 2026-06-22 02:50:25 +05:30
chat Add Open WebUI 0.10 compatibility (#543) 2026-06-30 21:51:14 +05:30
navigation Add refresh actions to empty sidebar tabs 2026-06-29 19:00:51 +05:30
notes Add refresh actions to empty sidebar tabs 2026-06-29 19:00:51 +05:30
notifications feat(notifications): in-app notification parity layer from socket events (#526) 2026-06-22 02:50:25 +05:30
profile Add native voice pipeline and voice UI polish 2026-06-24 18:53:06 +05:30
prompts refactor(dialogs): replace showDialog with ThemedDialogs for consistency 2026-05-13 10:55:31 +05:30
terminal Offline-first persistence with Drift (#508) 2026-06-19 15:08:16 +05:30
tools/providers feat(auth): enhance user authentication flow and cleanup 2026-05-18 19:13:39 +05:30