Skip to content
Source
Sign inSign up
Open History to see the last commit

BackgroundActivitySettings.tsx

442 lines17.9 KB

1import { useScopedSettingsWriteAllowed } from "./useScopedSettings";
2import { SettingsIcon } from "lucide-react";
3import { useState } from "react";
4import { type BackgroundActivityProfile } from "@t3tools/contracts";
5import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";
6import { resolveServerBackgroundActivitySettings } from "@t3tools/shared/backgroundActivitySettings";
7import * as Duration from "effect/Duration";
8import * as Equal from "effect/Equal";
9import {
10 useScopedSettings,
11 useScopedSettingsMixed,
12 useUpdateScopedSettings,
13} from "./useScopedSettings";
14import { useSettingsScope } from "./SettingsScopeContext";
15import { Button } from "../ui/button";
16import {
17 Dialog,
18 DialogDescription,
19 DialogFooter,
20 DialogHeader,
21 DialogPanel,
22 DialogPopup,
23 DialogTitle,
24} from "../ui/dialog";
25import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
26import { Switch } from "../ui/switch";
27import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
28import {
29 backgroundActivityOverrideSettings,
30 backgroundActivitySharedPolicySettings,
31 durationToSeconds,
32 PROVIDER_HEALTH_INTERVAL_STEP_SECONDS,
33 resolveBackgroundActivityProfileOption,
34} from "./SettingsPanels.logic";
35import { IntervalSecondsField } from "./IntervalSecondsField";
36import { PolicyTooltip, SettingResetButton, SettingsRow } from "./settingsLayout";
37import { searchableSetting } from "./settingsSearch";
38​
39const BACKGROUND_ACTIVITY_PROFILE_LABELS: Record<BackgroundActivityProfile, string> = {
40 balanced: "Balanced",
41 performance: "Performance",
42 "battery-saver": "Battery saver",
43};
44​
45type BackgroundActivityProfileOption = BackgroundActivityProfile | "advanced";
46​
47const BACKGROUND_ACTIVITY_PROFILE_OPTION_LABELS: Record<BackgroundActivityProfileOption, string> = {
48 ...BACKGROUND_ACTIVITY_PROFILE_LABELS,
49 advanced: "Advanced",
50};
51​
52const BACKGROUND_ACTIVITY_PROFILE_DESCRIPTIONS: Record<BackgroundActivityProfile, string> = {
53 balanced: "Pauses probes for idle clients, locked hosts, or low power mode.",
54 performance: "Allows scoped background probes while any subscribed client remains connected.",
55 "battery-saver": "Also pauses background probes when the host or client is on battery.",
56};
57​
58const ADVANCED_BACKGROUND_ACTIVITY_DESCRIPTION = "Uses custom intervals.";
59​
60const BACKGROUND_ACTIVITY_BOOLEAN_OVERRIDES: ReadonlyArray<{
61 readonly key:
62 | "pauseWhenHostLocked"
63 | "pauseWhenHostLowPower"
64 | "pauseWhenClientLowPower"
65 | "pauseWhenOnBattery";
66 readonly label: string;
67}> = [
68 { key: "pauseWhenHostLocked", label: "Pause when host is locked" },
69 { key: "pauseWhenHostLowPower", label: "Pause on host low power" },
70 { key: "pauseWhenClientLowPower", label: "Pause on client low power" },
71 { key: "pauseWhenOnBattery", label: "Pause on battery" },
72];
73​
74function resetBackgroundActivitySettings() {
75 return {
76 backgroundActivity: DEFAULT_UNIFIED_SETTINGS.backgroundActivity,
77 };
79​
80function backgroundActivityProfileSettings(profile: BackgroundActivityProfile) {
81 return {
82 backgroundActivity: {
83 schemaVersion: 1 as const,
84 profile,
85 overrides: {},
86 },
87 };
89​
90function BackgroundActivityAdvancedDialog({
91 open,
92 onOpenChange,
93}: {
94 readonly open: boolean;
95 readonly onOpenChange: (open: boolean) => void;
96}) {
97 const canWriteSettings = useScopedSettingsWriteAllowed();
98 const settings = useScopedSettings();
99 const updateSettings = useUpdateScopedSettings();
100 const resolvedBackgroundActivity = resolveServerBackgroundActivitySettings(settings);
101 const activeProfile = resolvedBackgroundActivity.profile;
102 const automaticGitFetchIntervalSeconds = durationToSeconds(
103 resolvedBackgroundActivity.automaticGitFetchInterval,
104 );
105 const providerHealthRefreshIntervalSeconds = durationToSeconds(
106 resolvedBackgroundActivity.providerHealthRefreshInterval,
107 );
108 const hostPowerMonitorActiveIntervalSeconds = durationToSeconds(
109 resolvedBackgroundActivity.hostPowerMonitorActiveInterval,
110 );
111 const hostPowerMonitorIdleIntervalSeconds = durationToSeconds(
112 resolvedBackgroundActivity.hostPowerMonitorIdleInterval,
113 );
114​
115 return (
116 <Dialog open={open && canWriteSettings} onOpenChange={onOpenChange}>
117 <DialogPopup className="max-w-xl">
118 <DialogHeader>
119 <DialogTitle>Background Activity</DialogTitle>
120 <DialogDescription>
121 Tune the shared power policy and the background intervals that feed it.
122 </DialogDescription>
123 </DialogHeader>
124 <DialogPanel>
125 <fieldset
126 disabled={!canWriteSettings}
127 className="min-w-0 overflow-hidden rounded-card border border-transparent surface-card-settings text-foreground"
128 >
129 <div className="flex flex-col gap-3 border-b border-border/50 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
130 <div className="min-w-0 space-y-1">
131 <div className="text-sm font-medium">Shared policy</div>
132 <p className="text-xs leading-relaxed text-muted-foreground">
133 Controls whether background work may run after a subscribed interval fires.
134 </p>
135 </div>
136 <Select
137 value={activeProfile}
138 onValueChange={(value) => {
139 if (
140 value === "balanced" ||
141 value === "performance" ||
142 value === "battery-saver"
143 ) {
144 updateSettings({
145 backgroundActivity: backgroundActivitySharedPolicySettings(settings, value),
146 });
147 }
148 }}
149 >
150 <SelectTrigger
151 variant="well"
152 size="compact"
153 className="w-full sm:w-40"
154 aria-label="Shared background policy"
155 >
156 <SelectValue>{BACKGROUND_ACTIVITY_PROFILE_LABELS[activeProfile]}</SelectValue>
157 </SelectTrigger>
158 <SelectPopup align="end" alignItemWithTrigger={false}>
159 <SelectItem value="balanced">
160 {BACKGROUND_ACTIVITY_PROFILE_LABELS.balanced}
161 </SelectItem>
162 <SelectItem value="performance">
163 {BACKGROUND_ACTIVITY_PROFILE_LABELS.performance}
164 </SelectItem>
165 <SelectItem value="battery-saver">
166 {BACKGROUND_ACTIVITY_PROFILE_LABELS["battery-saver"]}
167 </SelectItem>
168 </SelectPopup>
169 </Select>
170 </div>
171​
172 <div className="flex flex-col gap-3 border-b border-border/50 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
173 <div className="min-w-0 space-y-1">
174 <div className="text-sm font-medium">
175 {searchableSetting("git-fetch-interval").title}
176 </div>
177 <p className="text-xs leading-relaxed text-muted-foreground">
178 Refresh remote branch status in the background.
179 </p>
180 </div>
181 <div className="flex shrink-0 items-center gap-2">
182 <IntervalSecondsField
183 label="Git fetch interval"
184 value={automaticGitFetchIntervalSeconds}
185 min={0}
186 step={5}
187 onCommit={(seconds, options) =>
188 updateSettings(
189 backgroundActivityOverrideSettings(
190 settings.backgroundActivity,
191 resolvedBackgroundActivity,
192 { automaticGitFetchInterval: Duration.seconds(seconds) },
193 ),
194 options,
195 )
196 }
197 />
198 <span className="text-xs text-muted-foreground">seconds</span>
199 </div>
200 </div>
201​
202 <div className="flex flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
203 <div className="min-w-0 space-y-1">
204 <div className="text-sm font-medium">Provider health interval</div>
205 <p className="text-xs leading-relaxed text-muted-foreground">
206 Refresh provider availability, versions, and auth state. Model catalog updates
207 follow Provider update checks.
208 </p>
209 </div>
210 <div className="flex shrink-0 items-center gap-2">
211 <IntervalSecondsField
212 label="provider health interval"
213 value={providerHealthRefreshIntervalSeconds}
214 min={0}
215 step={PROVIDER_HEALTH_INTERVAL_STEP_SECONDS}
216 onCommit={(seconds, options) =>
217 updateSettings(
218 backgroundActivityOverrideSettings(
219 settings.backgroundActivity,
220 resolvedBackgroundActivity,
221 { providerHealthRefreshInterval: Duration.seconds(seconds) },
222 ),
223 options,
224 )
225 }
226 />
227 <span className="text-xs text-muted-foreground">seconds</span>
228 </div>
229 </div>
230​
231 <div className="flex flex-col gap-3 border-t border-border/50 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
232 <div className="min-w-0 space-y-1">
233 <div className="text-sm font-medium">Host power monitor</div>
234 <p className="text-xs leading-relaxed text-muted-foreground">
235 Poll host power state while clients are active.
236 </p>
237 </div>
238 <div className="flex shrink-0 items-center gap-2">
239 <IntervalSecondsField
240 label="active host power interval"
241 value={hostPowerMonitorActiveIntervalSeconds}
242 min={5}
243 step={5}
244 onCommit={(seconds, options) =>
245 updateSettings(
246 backgroundActivityOverrideSettings(
247 settings.backgroundActivity,
248 resolvedBackgroundActivity,
249 { hostPowerMonitorActiveInterval: Duration.seconds(seconds) },
250 ),
251 options,
252 )
253 }
254 />
255 <span className="text-xs text-muted-foreground">seconds</span>
256 </div>
257 </div>
258​
259 <div className="flex flex-col gap-3 border-t border-border/50 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
260 <div className="min-w-0 space-y-1">
261 <div className="text-sm font-medium">Idle host monitor</div>
262 <p className="text-xs leading-relaxed text-muted-foreground">
263 Poll host power state when no foreground client is active.
264 </p>
265 </div>
266 <div className="flex shrink-0 items-center gap-2">
267 <IntervalSecondsField
268 label="idle host power interval"
269 value={hostPowerMonitorIdleIntervalSeconds}
270 min={5}
271 step={30}
272 onCommit={(seconds, options) =>
273 updateSettings(
274 backgroundActivityOverrideSettings(
275 settings.backgroundActivity,
276 resolvedBackgroundActivity,
277 { hostPowerMonitorIdleInterval: Duration.seconds(seconds) },
278 ),
279 options,
280 )
281 }
282 />
283 <span className="text-xs text-muted-foreground">seconds</span>
284 </div>
285 </div>
286​
287 <div className="grid gap-0 border-t border-border/50 sm:grid-cols-2">
288 {BACKGROUND_ACTIVITY_BOOLEAN_OVERRIDES.map(({ key, label }) => (
289 <label
290 key={key}
291 className="flex items-center justify-between gap-3 border-b border-border/50 px-4 py-3 last:border-b-0 sm:border-r sm:even:border-r-0"
292 >
293 <span className="text-sm font-medium">{label}</span>
294 <Switch
295 checked={resolvedBackgroundActivity[key]}
296 onCheckedChange={(checked) =>
297 updateSettings(
298 backgroundActivityOverrideSettings(
299 settings.backgroundActivity,
300 resolvedBackgroundActivity,
301 {
302 [key]: Boolean(checked),
303 },
304 ),
305 )
306 }
307 aria-label={label}
308 />
309 </label>
310 ))}
311 </div>
312 </fieldset>
313 </DialogPanel>
314 <DialogFooter>
315 <Button
316 variant="secondary"
317 disabled={!canWriteSettings}
318 onClick={() => updateSettings(resetBackgroundActivitySettings())}
319 >
320 Reset all
321 </Button>
322 <Button onClick={() => onOpenChange(false)}>Done</Button>
323 </DialogFooter>
324 </DialogPopup>
325 </Dialog>
326 );
328​
329export function BackgroundActivitySettingsRow() {
330 const settings = useScopedSettings();
331 const updateSettings = useUpdateScopedSettings();
332 const { scope, environment } = useSettingsScope();
333 const environmentId = environment?.environmentId ?? null;
334 const isEnvironmentScope = scope.environmentIds.length === 1 && environmentId !== null;
335 const [backgroundActivityDialogOpen, setBackgroundActivityDialogOpen] = useState(false);
336 const mixedBackgroundActivity = useScopedSettingsMixed(["backgroundActivity"]);
337 const resolvedBackgroundActivity = resolveServerBackgroundActivitySettings(settings);
338 const activeBackgroundActivityProfile = resolvedBackgroundActivity.profile;
339 const backgroundActivityProfileOption = resolveBackgroundActivityProfileOption(settings);
340 const backgroundActivityDescription =
341 backgroundActivityProfileOption === "advanced"
342 ? `${ADVANCED_BACKGROUND_ACTIVITY_DESCRIPTION} Shared policy: ${
343 BACKGROUND_ACTIVITY_PROFILE_LABELS[activeBackgroundActivityProfile]
344 }.`
345 : BACKGROUND_ACTIVITY_PROFILE_DESCRIPTIONS[resolvedBackgroundActivity.profile];
346 const canResetBackgroundActivity = !Equal.equals(
347 settings.backgroundActivity,
348 DEFAULT_UNIFIED_SETTINGS.backgroundActivity,
349 );
350​
351 return (
352 <SettingsRow
353 serverScoped
354 settingKeys={["backgroundActivity"]}
355 id={searchableSetting("background-activity").id}
356 title={
357 <span className="inline-flex items-center gap-1.5">
358 {searchableSetting("background-activity").title}
359 <PolicyTooltip>
360 This shared policy gates background work such as Git refreshes and provider health
361 probes after their individual intervals elapse.
362 </PolicyTooltip>
363 </span>
364 }
365 description={backgroundActivityDescription}
366 resetAction={
367 canResetBackgroundActivity ? (
368 <SettingResetButton
369 label="background activity"
370 onClick={() => updateSettings(resetBackgroundActivitySettings())}
371 />
372 ) : null
373 }
374 control={
375 <>
376 <Select
377 value={mixedBackgroundActivity ? null : backgroundActivityProfileOption}
378 onValueChange={(value) => {
379 if (value === "advanced") {
380 if (isEnvironmentScope) setBackgroundActivityDialogOpen(true);
381 return;
382 }
383 if (value === "balanced" || value === "performance" || value === "battery-saver") {
384 updateSettings(backgroundActivityProfileSettings(value));
385 }
386 }}
387 >
388 <SelectTrigger
389 variant="well"
390 size="compact"
391 className="w-full sm:w-40"
392 aria-label="Background activity profile"
393 >
394 <SelectValue>
395 {(value: BackgroundActivityProfileOption | null) =>
396 value === null ? "Mixed" : BACKGROUND_ACTIVITY_PROFILE_OPTION_LABELS[value]
397 }
398 </SelectValue>
399 </SelectTrigger>
400 <SelectPopup align="end" alignItemWithTrigger={false}>
401 <SelectItem value="balanced">
402 {BACKGROUND_ACTIVITY_PROFILE_LABELS.balanced}
403 </SelectItem>
404 <SelectItem value="performance">
405 {BACKGROUND_ACTIVITY_PROFILE_LABELS.performance}
406 </SelectItem>
407 <SelectItem value="battery-saver">
408 {BACKGROUND_ACTIVITY_PROFILE_LABELS["battery-saver"]}
409 </SelectItem>
410 <SelectItem value="advanced" disabled={!isEnvironmentScope}>
411 {isEnvironmentScope
412 ? BACKGROUND_ACTIVITY_PROFILE_OPTION_LABELS.advanced
413 : `${BACKGROUND_ACTIVITY_PROFILE_OPTION_LABELS.advanced} (one environment)`}
414 </SelectItem>
415 </SelectPopup>
416 </Select>
417 {backgroundActivityProfileOption === "advanced" && isEnvironmentScope ? (
418 <Tooltip>
419 <TooltipTrigger
420 render={
421 <Button
422 size="icon-sm"
423 variant="secondary"
424 aria-label="Configure advanced background activity"
425 onClick={() => setBackgroundActivityDialogOpen(true)}
426 >
427 <SettingsIcon className="size-4" />
428 </Button>
429 }
430 />
431 <TooltipPopup side="top">Configure background activity</TooltipPopup>
432 </Tooltip>
433 ) : null}
434 <BackgroundActivityAdvancedDialog
435 open={backgroundActivityDialogOpen && isEnvironmentScope}
436 onOpenChange={setBackgroundActivityDialogOpen}
437 />
438 </>
439 }
440 />
441 );