authorgravatar for lucenz@proton.meSimone <lucenz@proton.me> 2026-09-03 18:35:25+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 09:35:25-07:00
logcfddb4201df8941bbfde008919da70fe5ec7552b
treed75d4518e6deb7303cdd7220b787a2f1ab3d715c
parent57626eb6eaa436b22260068d23f4e3df5f389cd1
signature Signed by PGP key B5690EEEBB952194

fix(mobile): skip unsupported shared settings targets (#9381)


6 files changed, 119 insertions(+), 69 deletions(-)

apps/mobile/src/features/settings/SettingsRouteScreen.tsx+9-12
...@@ -46,6 +46,7 @@ import {...@@ -46,6 +46,7 @@ import {
46import {46import {
47 findSharedSettingsMismatches,47 findSharedSettingsMismatches,
48 pickSharedServerSettings,48 pickSharedServerSettings,
49 supportsSharedSettingsSync,
49} from "@t3tools/client-runtime/state/shared-settings";50} from "@t3tools/client-runtime/state/shared-settings";
50import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";51import { useThreadListV2Enabled } from "../threads/use-thread-list-v2-enabled";
51import {52import {
...@@ -553,9 +554,9 @@ const AUTO_SETTLE_DEFAULT_DAYS = DEFAULT_SERVER_SETTINGS.sidebarAutoSettleAfterD...@@ -553,9 +554,9 @@ const AUTO_SETTLE_DEFAULT_DAYS = DEFAULT_SERVER_SETTINGS.sidebarAutoSettleAfterD
553554
554/**555/**
555 * Auto-settlement is a user preference that every server has to hold. Mobile556 * Auto-settlement is a user preference that every server has to hold. Mobile
556 * has no primary environment, so the first connected environment that557 * has no primary environment, so the first eligible sync target provides the
557 * supports it is the reference value. Edits fan out to every connected558 * reference value. Edits fan out to every eligible target, and a mismatch row
558 * environment, and a mismatch row lets the user push the reference out.559 * lets the user push the reference out.
559 */560 */
560function AutoSettleSettingsRows() {561function AutoSettleSettingsRows() {
561 const { environments } = useEnvironments();562 const { environments } = useEnvironments();
...@@ -564,12 +565,8 @@ function AutoSettleSettingsRows() {...@@ -564,12 +565,8 @@ function AutoSettleSettingsRows() {
564 reportFailure: true,565 reportFailure: true,
565 });566 });
566567
567 const connected = environments.filter(568 const syncTargets = environments.filter(supportsSharedSettingsSync);
568 (environment) =>569 const reference = syncTargets[0] ?? null;
569 environment.connection.phase === "connected" &&
570 environment.serverConfig?.environment.capabilities.threadAutoSettlement === true,
571 );
572 const reference = connected[0] ?? null;
573 const referenceSettings = reference?.serverConfig?.settings ?? null;570 const referenceSettings = reference?.serverConfig?.settings ?? null;
574571
575 const [daysDraft, setDaysDraft] = useState<string | null>(null);572 const [daysDraft, setDaysDraft] = useState<string | null>(null);
...@@ -579,7 +576,7 @@ function AutoSettleSettingsRows() {...@@ -579,7 +576,7 @@ function AutoSettleSettingsRows() {
579 }576 }
580577
581 const writeToAll = (patch: ServerSettingsPatch) => {578 const writeToAll = (patch: ServerSettingsPatch) => {
582 for (const environment of connected) {579 for (const environment of syncTargets) {
583 void updateSettings({ environmentId: environment.environmentId, input: { patch } });580 void updateSettings({ environmentId: environment.environmentId, input: { patch } });
584 }581 }
585 };582 };
...@@ -590,7 +587,7 @@ function AutoSettleSettingsRows() {...@@ -590,7 +587,7 @@ function AutoSettleSettingsRows() {
590 environments: environments.map((environment) => ({587 environments: environments.map((environment) => ({
591 environmentId: environment.environmentId,588 environmentId: environment.environmentId,
592 label: environment.label,589 label: environment.label,
593 connected: environment.connection.phase === "connected",590 syncEligible: supportsSharedSettingsSync(environment),
594 settings: environment.serverConfig?.settings ?? null,591 settings: environment.serverConfig?.settings ?? null,
595 })),592 })),
596 });593 });
...@@ -600,7 +597,7 @@ function AutoSettleSettingsRows() {...@@ -600,7 +597,7 @@ function AutoSettleSettingsRows() {
600 const draft = (daysDraft ?? "").trim();597 const draft = (daysDraft ?? "").trim();
601 setDaysDraft(null);598 setDaysDraft(null);
602 // Whole-string check so "3.5" and "3days" are rejected instead of599 // Whole-string check so "3.5" and "3days" are rejected instead of
603 // silently becoming 3 on every connected environment.600 // silently becoming 3 on every eligible sync target.
604 const parsed = /^\d+$/.test(draft) ? Number(draft) : Number.NaN;601 const parsed = /^\d+$/.test(draft) ? Number(draft) : Number.NaN;
605 if (602 if (
606 Number.isInteger(parsed) &&603 Number.isInteger(parsed) &&
apps/web/src/hooks/useSettings.ts+15-30
...@@ -29,6 +29,7 @@ import {...@@ -29,6 +29,7 @@ import {
29 findSharedSettingsMismatches,29 findSharedSettingsMismatches,
30 pickSharedServerSettings,30 pickSharedServerSettings,
31 splitSharedServerPatch,31 splitSharedServerPatch,
32 supportsSharedSettingsSync,
32} from "@t3tools/client-runtime/state/shared-settings";33} from "@t3tools/client-runtime/state/shared-settings";
33import { ensureLocalApi } from "~/localApi";34import { ensureLocalApi } from "~/localApi";
34import {35import {
...@@ -42,11 +43,7 @@ import * as Struct from "effect/Struct";...@@ -42,11 +43,7 @@ import * as Struct from "effect/Struct";
42import { toastManager } from "~/components/ui/toast";43import { toastManager } from "~/components/ui/toast";
43import { isHostedStaticApp } from "~/hostedPairing";44import { isHostedStaticApp } from "~/hostedPairing";
44import { primaryServerSettingsAtom, serverEnvironment } from "~/state/server";45import { primaryServerSettingsAtom, serverEnvironment } from "~/state/server";
45import {46import { useEnvironments, usePrimaryEnvironment } from "~/state/environments";
46 type EnvironmentPresentation,
47 useEnvironments,
48 usePrimaryEnvironment,
49} from "~/state/environments";
50import { useAtomCommand } from "~/state/use-atom-command";47import { useAtomCommand } from "~/state/use-atom-command";
51import { useTheme } from "./useTheme";48import { useTheme } from "./useTheme";
5249
...@@ -332,26 +329,14 @@ export function usePrimarySettingsAvailable(): boolean {...@@ -332,26 +329,14 @@ export function usePrimarySettingsAvailable(): boolean {
332 return primaryEnvironment !== null || !isHostedStaticApp();329 return primaryEnvironment !== null || !isHostedStaticApp();
333}330}
334331
335/**
336 * Whether an environment can hold every shared key right now. Gated on the
337 * auto-settlement capability because it is the newest of the shared keys: a
338 * server that has it has all of them. Older servers drop unknown keys on
339 * write, so a mismatch against them could never clear, and their decoded
340 * defaults must not be treated as real values.
341 */
342function supportsSharedSettings(environment: EnvironmentPresentation): boolean {
343 return (
344 environment.connection.phase === "connected" &&
345 environment.serverConfig?.environment.capabilities.threadAutoSettlement === true
346 );
347}
348
349/** Environments that can receive a shared settings write right now. */332/** Environments that can receive a shared settings write right now. */
350function useConnectedEnvironmentIds(): ReadonlyArray<EnvironmentId> {333function useSharedSettingsSyncTargetIds(): ReadonlyArray<EnvironmentId> {
351 const { environments } = useEnvironments();334 const { environments } = useEnvironments();
352 return useMemo(335 return useMemo(
353 () =>336 () =>
354 environments.filter(supportsSharedSettings).map((environment) => environment.environmentId),337 environments
338 .filter(supportsSharedSettingsSync)
339 .map((environment) => environment.environmentId),
355 [environments],340 [environments],
356 );341 );
357}342}
...@@ -361,16 +346,16 @@ function useConnectedEnvironmentIds(): ReadonlyArray<EnvironmentId> {...@@ -361,16 +346,16 @@ function useConnectedEnvironmentIds(): ReadonlyArray<EnvironmentId> {
361 *346 *
362 * Server keys are optimistically patched in atom-backed server state, then347 * Server keys are optimistically patched in atom-backed server state, then
363 * persisted via RPC. Shared server keys (see `SHARED_SERVER_SETTING_KEYS`)348 * persisted via RPC. Shared server keys (see `SHARED_SERVER_SETTING_KEYS`)
364 * are written to every connected environment, not only the target, so a user349 * are written to every eligible sync target, not only the selected target, so
365 * preference does not silently drift between machines. Client keys go through350 * a user preference does not silently drift between machines. Client keys go
366 * client persistence.351 * through client persistence.
367 */352 */
368function useUpdateSettingsTarget(environmentId: EnvironmentId | null) {353function useUpdateSettingsTarget(environmentId: EnvironmentId | null) {
369 const persistServerSettings = useAtomCommand(354 const persistServerSettings = useAtomCommand(
370 serverEnvironment.updateSettings,355 serverEnvironment.updateSettings,
371 "server settings update",356 "server settings update",
372 );357 );
373 const connectedEnvironmentIds = useConnectedEnvironmentIds();358 const sharedSettingsSyncTargetIds = useSharedSettingsSyncTargetIds();
374 const updateSettings = useCallback(359 const updateSettings = useCallback(
375 (patch: UnifiedSettingsPatch) => {360 (patch: UnifiedSettingsPatch) => {
376 const { serverPatch, clientPatch } = splitPatch(patch);361 const { serverPatch, clientPatch } = splitPatch(patch);
...@@ -395,7 +380,7 @@ function useUpdateSettingsTarget(environmentId: EnvironmentId | null) {...@@ -395,7 +380,7 @@ function useUpdateSettingsTarget(environmentId: EnvironmentId | null) {
395 }380 }
396 }381 }
397 if (Object.keys(sharedPatch).length > 0) {382 if (Object.keys(sharedPatch).length > 0) {
398 const targets = new Set(connectedEnvironmentIds);383 const targets = new Set(sharedSettingsSyncTargetIds);
399 if (environmentId) {384 if (environmentId) {
400 targets.add(environmentId);385 targets.add(environmentId);
401 }386 }
...@@ -417,14 +402,14 @@ function useUpdateSettingsTarget(environmentId: EnvironmentId | null) {...@@ -417,14 +402,14 @@ function useUpdateSettingsTarget(environmentId: EnvironmentId | null) {
417 });402 });
418 }403 }
419 },404 },
420 [connectedEnvironmentIds, environmentId, persistServerSettings],405 [environmentId, persistServerSettings, sharedSettingsSyncTargetIds],
421 );406 );
422407
423 return updateSettings;408 return updateSettings;
424}409}
425410
426/**411/**
427 * Connected environments whose shared settings differ from the primary's,412 * Shared-settings sync targets whose values differ from the primary's,
428 * plus an action that writes the primary's values to all of them. Drift413 * plus an action that writes the primary's values to all of them. Drift
429 * happens when an environment was offline during an edit or was changed by414 * happens when an environment was offline during an edit or was changed by
430 * an older client.415 * an older client.
...@@ -437,7 +422,7 @@ export function useSharedSettingsSync() {...@@ -437,7 +422,7 @@ export function useSharedSettingsSync() {
437 // must never push defaults over real values. Same for a primary too old to422 // must never push defaults over real values. Same for a primary too old to
438 // hold the shared keys: its decoded defaults are not a source of truth.423 // hold the shared keys: its decoded defaults are not a source of truth.
439 const primarySettings =424 const primarySettings =
440 primaryEnvironment !== null && supportsSharedSettings(primaryEnvironment)425 primaryEnvironment !== null && supportsSharedSettingsSync(primaryEnvironment)
441 ? (primaryEnvironment.serverConfig?.settings ?? null)426 ? (primaryEnvironment.serverConfig?.settings ?? null)
442 : null;427 : null;
443 const { environments } = useEnvironments();428 const { environments } = useEnvironments();
...@@ -454,7 +439,7 @@ export function useSharedSettingsSync() {...@@ -454,7 +439,7 @@ export function useSharedSettingsSync() {
454 environments: environments.map((environment) => ({439 environments: environments.map((environment) => ({
455 environmentId: environment.environmentId,440 environmentId: environment.environmentId,
456 label: environment.label,441 label: environment.label,
457 connected: supportsSharedSettings(environment),442 syncEligible: supportsSharedSettingsSync(environment),
458 settings: environment.serverConfig?.settings ?? null,443 settings: environment.serverConfig?.settings ?? null,
459 })),444 })),
460 }),445 }),
docs/internals/overview.md+4-3
...@@ -90,9 +90,10 @@ A turn is complete when its session leaves `running` status, projected by...@@ -90,9 +90,10 @@ A turn is complete when its session leaves `running` status, projected by
90does not define turn end.90does not define turn end.
9191
92Thread settlement is server-owned. Each server's own settings control PR and inactivity92Thread settlement is server-owned. Each server's own settings control PR and inactivity
93settlement. Those keys are user preferences, so clients write them to every connected environment93settlement. Those keys are user preferences, so clients write them to every shared-settings sync
94(`SHARED_SERVER_SETTING_KEYS` in `packages/client-runtime/src/state/sharedSettings.ts`) and warn94target (`SHARED_SERVER_SETTING_KEYS` in `packages/client-runtime/src/state/sharedSettings.ts`) and
95when a connected environment drifts.95warn when another target drifts. A target must have an active connection and advertise the
96`threadAutoSettlement` capability, which signals that the server can hold every shared key.
96[`ThreadSettlementReactor`][settlement] checks threads at startup, when those settings change, and97[`ThreadSettlementReactor`][settlement] checks threads at startup, when those settings change, and
97once per minute, including when no client is connected. It dispatches the guarded internal98once per minute, including when no client is connected. It dispatches the guarded internal
98`thread.auto-settle` command, which uses the existing settlement event lifecycle. Automatic99`thread.auto-settle` command, which uses the existing settlement event lifecycle. Automatic
docs/user/thread-sidebar.md+7-5
...@@ -22,11 +22,13 @@ available, the inactivity rule still applies. A manual un-settle also keeps the...@@ -22,11 +22,13 @@ available, the inactivity rule still applies. A manual un-settle also keeps the
22sorts by the moment you settled it. A thread that settled on its own sorts by its last message or22sorts by the moment you settled it. A thread that settled on its own sorts by its last message or
23turn, not by when the server noticed it was inactive.23turn, not by when the server noticed it was inactive.
2424
25Change these rules in **Settings > General**. The change is written to every environment you are25Change these rules in **Settings > General**. The change is written to every connected environment
26connected to at that moment. An environment that is offline keeps its old value. When a connected26whose server supports shared settings. An environment that is offline or needs a server update
27environment holds a different value, **Settings > General** shows a warning that names it. Choose27keeps its old value and does not appear in mismatch warnings. When a connected environment whose
28**Apply to all** to write your current values to every connected environment. The same applies to28server supports shared settings holds a different value, **Settings > General** shows a warning
29the new-thread workspace mode and the source control writing style.29that names it. Choose **Apply to all** to write your current values to the environments named in
30the warning. The same applies to the new-thread workspace mode and the source control writing
31style.
3032
31A settings change affects future settlement and does not reopen a settled thread. Settings saved33A settings change affects future settlement and does not reopen a settled thread. Settings saved
32by older clients on one device no longer control this behavior.34by older clients on one device no longer control this behavior.
packages/client-runtime/src/state/sharedSettings.test.ts+48-9
...@@ -5,12 +5,36 @@ import {...@@ -5,12 +5,36 @@ import {
5 findSharedSettingsMismatches,5 findSharedSettingsMismatches,
6 pickSharedServerSettings,6 pickSharedServerSettings,
7 splitSharedServerPatch,7 splitSharedServerPatch,
8 supportsSharedSettingsSync,
8} from "./sharedSettings.ts";9} from "./sharedSettings.ts";
910
10const primaryId = EnvironmentId.make("env-primary");11const primaryId = EnvironmentId.make("env-primary");
11const laptopId = EnvironmentId.make("env-laptop");12const laptopId = EnvironmentId.make("env-laptop");
12const boxId = EnvironmentId.make("env-box");13const boxId = EnvironmentId.make("env-box");
1314
15describe("supportsSharedSettingsSync", () => {
16 it("accepts only connected servers that advertise the shared-settings capability", () => {
17 expect(
18 supportsSharedSettingsSync({
19 connection: { phase: "connected" },
20 serverConfig: { environment: { capabilities: { threadAutoSettlement: true } } },
21 }),
22 ).toBe(true);
23 expect(
24 supportsSharedSettingsSync({
25 connection: { phase: "connected" },
26 serverConfig: { environment: { capabilities: {} } },
27 }),
28 ).toBe(false);
29 expect(
30 supportsSharedSettingsSync({
31 connection: { phase: "reconnecting" },
32 serverConfig: { environment: { capabilities: { threadAutoSettlement: true } } },
33 }),
34 ).toBe(false);
35 });
36});
37
14describe("splitSharedServerPatch", () => {38describe("splitSharedServerPatch", () => {
15 it("routes preference keys to the shared patch and machine keys to the local patch", () => {39 it("routes preference keys to the shared patch and machine keys to the local patch", () => {
16 const { sharedPatch, localPatch } = splitSharedServerPatch({40 const { sharedPatch, localPatch } = splitSharedServerPatch({
...@@ -38,17 +62,27 @@ describe("pickSharedServerSettings", () => {...@@ -38,17 +62,27 @@ describe("pickSharedServerSettings", () => {
38describe("findSharedSettingsMismatches", () => {62describe("findSharedSettingsMismatches", () => {
39 const primarySettings = { ...DEFAULT_SERVER_SETTINGS, sidebarAutoSettleAfterDays: 7 };63 const primarySettings = { ...DEFAULT_SERVER_SETTINGS, sidebarAutoSettleAfterDays: 7 };
4064
41 it("lists connected environments whose shared settings differ", () => {65 it("lists sync-eligible environments whose shared settings differ", () => {
42 const mismatches = findSharedSettingsMismatches({66 const mismatches = findSharedSettingsMismatches({
43 primaryEnvironmentId: primaryId,67 primaryEnvironmentId: primaryId,
44 primarySettings,68 primarySettings,
45 environments: [69 environments: [
46 { environmentId: primaryId, label: "Desktop", connected: true, settings: primarySettings },70 {
47 { environmentId: laptopId, label: "Laptop", connected: true, settings: primarySettings },71 environmentId: primaryId,
72 label: "Desktop",
73 syncEligible: true,
74 settings: primarySettings,
75 },
76 {
77 environmentId: laptopId,
78 label: "Laptop",
79 syncEligible: true,
80 settings: primarySettings,
81 },
48 {82 {
49 environmentId: boxId,83 environmentId: boxId,
50 label: "Remote Box",84 label: "Remote Box",
51 connected: true,85 syncEligible: true,
52 settings: DEFAULT_SERVER_SETTINGS,86 settings: DEFAULT_SERVER_SETTINGS,
53 },87 },
54 ],88 ],
...@@ -64,7 +98,7 @@ describe("findSharedSettingsMismatches", () => {...@@ -64,7 +98,7 @@ describe("findSharedSettingsMismatches", () => {
64 {98 {
65 environmentId: boxId,99 environmentId: boxId,
66 label: "Remote Box",100 label: "Remote Box",
67 connected: true,101 syncEligible: true,
68 settings: { ...primarySettings, enableAgentBrowserAccess: false },102 settings: { ...primarySettings, enableAgentBrowserAccess: false },
69 },103 },
70 ],104 ],
...@@ -74,7 +108,12 @@ describe("findSharedSettingsMismatches", () => {...@@ -74,7 +108,12 @@ describe("findSharedSettingsMismatches", () => {
74108
75 it("reports nothing until the primary environment's settings are loaded", () => {109 it("reports nothing until the primary environment's settings are loaded", () => {
76 const environments = [110 const environments = [
77 { environmentId: boxId, label: "Remote Box", connected: true, settings: primarySettings },111 {
112 environmentId: boxId,
113 label: "Remote Box",
114 syncEligible: true,
115 settings: primarySettings,
116 },
78 ];117 ];
79 expect(118 expect(
80 findSharedSettingsMismatches({ primaryEnvironmentId: null, primarySettings, environments }),119 findSharedSettingsMismatches({ primaryEnvironmentId: null, primarySettings, environments }),
...@@ -88,7 +127,7 @@ describe("findSharedSettingsMismatches", () => {...@@ -88,7 +127,7 @@ describe("findSharedSettingsMismatches", () => {
88 ).toEqual([]);127 ).toEqual([]);
89 });128 });
90129
91 it("skips offline environments and environments without a loaded config", () => {130 it("skips ineligible environments and environments without a loaded config", () => {
92 const mismatches = findSharedSettingsMismatches({131 const mismatches = findSharedSettingsMismatches({
93 primaryEnvironmentId: primaryId,132 primaryEnvironmentId: primaryId,
94 primarySettings,133 primarySettings,
...@@ -96,10 +135,10 @@ describe("findSharedSettingsMismatches", () => {...@@ -96,10 +135,10 @@ describe("findSharedSettingsMismatches", () => {
96 {135 {
97 environmentId: laptopId,136 environmentId: laptopId,
98 label: "Laptop",137 label: "Laptop",
99 connected: false,138 syncEligible: false,
100 settings: DEFAULT_SERVER_SETTINGS,139 settings: DEFAULT_SERVER_SETTINGS,
101 },140 },
102 { environmentId: boxId, label: "Remote Box", connected: true, settings: null },141 { environmentId: boxId, label: "Remote Box", syncEligible: true, settings: null },
103 ],142 ],
104 });143 });
105 expect(mismatches).toEqual([]);144 expect(mismatches).toEqual([]);
packages/client-runtime/src/state/sharedSettings.ts+36-10
...@@ -4,14 +4,21 @@...@@ -4,14 +4,21 @@
4 * Every server keeps its own `settings.json`, but some keys are user4 * Every server keeps its own `settings.json`, but some keys are user
5 * preferences that only live on the server because the server has to act on5 * preferences that only live on the server because the server has to act on
6 * them (auto-settlement runs with no client attached). A user does not want6 * them (auto-settlement runs with no client attached). A user does not want
7 * those to differ per machine. Clients write these keys to every connected7 * those to differ per machine. Clients write these keys to every shared-settings
8 * environment, and warn when a connected environment still holds a different8 * sync target, and warn when another target still holds a different value so
9 * value so the user can push their current value out.9 * the user can push their current value out.
10 */10 */
11import type { EnvironmentId, ServerSettings, ServerSettingsPatch } from "@t3tools/contracts";11import type {
12 EnvironmentId,
13 ExecutionEnvironmentCapabilities,
14 ServerSettings,
15 ServerSettingsPatch,
16} from "@t3tools/contracts";
12import * as Equal from "effect/Equal";17import * as Equal from "effect/Equal";
13import * as Struct from "effect/Struct";18import * as Struct from "effect/Struct";
1419
20import type { EnvironmentConnectionPhase } from "../connection/presentation.ts";
21
15/** Server keys that hold a user preference rather than machine config. */22/** Server keys that hold a user preference rather than machine config. */
16export const SHARED_SERVER_SETTING_KEYS = [23export const SHARED_SERVER_SETTING_KEYS = [
17 "sidebarAutoSettleAfterDays",24 "sidebarAutoSettleAfterDays",
...@@ -50,18 +57,37 @@ export function pickSharedServerSettings(settings: ServerSettings): ServerSettin...@@ -50,18 +57,37 @@ export function pickSharedServerSettings(settings: ServerSettings): ServerSettin
50 return Struct.pick(settings, SHARED_SERVER_SETTING_KEYS);57 return Struct.pick(settings, SHARED_SERVER_SETTING_KEYS);
51}58}
5259
60/**
61 * Whether an environment can participate in shared-settings sync right now.
62 * Auto-settlement is the newest feature backed by a shared key, so a server
63 * advertising `threadAutoSettlement` can hold every shared key.
64 */
65export function supportsSharedSettingsSync(environment: {
66 readonly connection: { readonly phase: EnvironmentConnectionPhase };
67 readonly serverConfig: {
68 readonly environment: {
69 readonly capabilities: Pick<ExecutionEnvironmentCapabilities, "threadAutoSettlement">;
70 };
71 } | null;
72}): boolean {
73 return (
74 environment.connection.phase === "connected" &&
75 environment.serverConfig?.environment.capabilities.threadAutoSettlement === true
76 );
77}
78
53export interface SharedSettingsEnvironment {79export interface SharedSettingsEnvironment {
54 readonly environmentId: EnvironmentId;80 readonly environmentId: EnvironmentId;
55 readonly label: string;81 readonly label: string;
56 readonly connected: boolean;82 readonly syncEligible: boolean;
57 readonly settings: ServerSettings | null;83 readonly settings: ServerSettings | null;
58}84}
5985
60/**86/**
61 * Connected environments whose shared settings differ from the primary87 * Shared-settings sync targets whose values differ from the primary
62 * environment's. Offline environments are skipped: nothing can be read from88 * environment's. Other environments are skipped: nothing can be read from or
63 * or written to them, and the warning would never clear. With no primary89 * written to them, or their server cannot hold every shared key. With no
64 * settings loaded there is nothing to compare against, so nothing is90 * primary settings loaded there is nothing to compare against, so nothing is
65 * reported. Callers must pass the real loaded settings, never a default91 * reported. Callers must pass the real loaded settings, never a default
66 * fallback, or "apply to all" would push defaults over real values.92 * fallback, or "apply to all" would push defaults over real values.
67 */93 */
...@@ -77,7 +103,7 @@ export function findSharedSettingsMismatches(input: {...@@ -77,7 +103,7 @@ export function findSharedSettingsMismatches(input: {
77 return input.environments.flatMap((environment) => {103 return input.environments.flatMap((environment) => {
78 if (104 if (
79 environment.environmentId === input.primaryEnvironmentId ||105 environment.environmentId === input.primaryEnvironmentId ||
80 !environment.connected ||106 !environment.syncEligible ||
81 environment.settings === null107 environment.settings === null
82 ) {108 ) {
83 return [];109 return [];