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

BackgroundActivitySettings.tsx

440 lines17.8 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-xl border bg-card text-card-foreground"
128 >
129 <div className="flex flex-col gap-3 border-b 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 size="sm"
152 className="w-full sm:w-40"
153 aria-label="Shared background policy"
154 >
155 <SelectValue>{BACKGROUND_ACTIVITY_PROFILE_LABELS[activeProfile]}</SelectValue>
156 </SelectTrigger>
157 <SelectPopup align="end" alignItemWithTrigger={false}>
158 <SelectItem hideIndicator value="balanced">
159 {BACKGROUND_ACTIVITY_PROFILE_LABELS.balanced}
160 </SelectItem>
161 <SelectItem hideIndicator value="performance">
162 {BACKGROUND_ACTIVITY_PROFILE_LABELS.performance}
163 </SelectItem>
164 <SelectItem hideIndicator value="battery-saver">
165 {BACKGROUND_ACTIVITY_PROFILE_LABELS["battery-saver"]}
166 </SelectItem>
167 </SelectPopup>
168 </Select>
169 </div>
170​
171 <div className="flex flex-col gap-3 border-b px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
172 <div className="min-w-0 space-y-1">
173 <div className="text-sm font-medium">
174 {searchableSetting("git-fetch-interval").title}
175 </div>
176 <p className="text-xs leading-relaxed text-muted-foreground">
177 Refresh remote branch status in the background.
178 </p>
179 </div>
180 <div className="flex shrink-0 items-center gap-2">
181 <IntervalSecondsField
182 label="Git fetch interval"
183 value={automaticGitFetchIntervalSeconds}
184 min={0}
185 step={5}
186 onCommit={(seconds, options) =>
187 updateSettings(
188 backgroundActivityOverrideSettings(
189 settings.backgroundActivity,
190 resolvedBackgroundActivity,
191 { automaticGitFetchInterval: Duration.seconds(seconds) },
192 ),
193 options,
194 )
195 }
196 />
197 <span className="text-xs text-muted-foreground">seconds</span>
198 </div>
199 </div>
200​
201 <div className="flex flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
202 <div className="min-w-0 space-y-1">
203 <div className="text-sm font-medium">Provider health interval</div>
204 <p className="text-xs leading-relaxed text-muted-foreground">
205 Refresh provider availability, versions, and auth state. Model catalog updates
206 follow Provider update checks.
207 </p>
208 </div>
209 <div className="flex shrink-0 items-center gap-2">
210 <IntervalSecondsField
211 label="provider health interval"
212 value={providerHealthRefreshIntervalSeconds}
213 min={0}
214 step={PROVIDER_HEALTH_INTERVAL_STEP_SECONDS}
215 onCommit={(seconds, options) =>
216 updateSettings(
217 backgroundActivityOverrideSettings(
218 settings.backgroundActivity,
219 resolvedBackgroundActivity,
220 { providerHealthRefreshInterval: Duration.seconds(seconds) },
221 ),
222 options,
223 )
224 }
225 />
226 <span className="text-xs text-muted-foreground">seconds</span>
227 </div>
228 </div>
229​
230 <div className="flex flex-col gap-3 border-t px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
231 <div className="min-w-0 space-y-1">
232 <div className="text-sm font-medium">Host power monitor</div>
233 <p className="text-xs leading-relaxed text-muted-foreground">
234 Poll host power state while clients are active.
235 </p>
236 </div>
237 <div className="flex shrink-0 items-center gap-2">
238 <IntervalSecondsField
239 label="active host power interval"
240 value={hostPowerMonitorActiveIntervalSeconds}
241 min={5}
242 step={5}
243 onCommit={(seconds, options) =>
244 updateSettings(
245 backgroundActivityOverrideSettings(
246 settings.backgroundActivity,
247 resolvedBackgroundActivity,
248 { hostPowerMonitorActiveInterval: Duration.seconds(seconds) },
249 ),
250 options,
251 )
252 }
253 />
254 <span className="text-xs text-muted-foreground">seconds</span>
255 </div>
256 </div>
257​
258 <div className="flex flex-col gap-3 border-t px-4 py-3 sm:flex-row sm:items-center sm:justify-between">
259 <div className="min-w-0 space-y-1">
260 <div className="text-sm font-medium">Idle host monitor</div>
261 <p className="text-xs leading-relaxed text-muted-foreground">
262 Poll host power state when no foreground client is active.
263 </p>
264 </div>
265 <div className="flex shrink-0 items-center gap-2">
266 <IntervalSecondsField
267 label="idle host power interval"
268 value={hostPowerMonitorIdleIntervalSeconds}
269 min={5}
270 step={30}
271 onCommit={(seconds, options) =>
272 updateSettings(
273 backgroundActivityOverrideSettings(
274 settings.backgroundActivity,
275 resolvedBackgroundActivity,
276 { hostPowerMonitorIdleInterval: Duration.seconds(seconds) },
277 ),
278 options,
279 )
280 }
281 />
282 <span className="text-xs text-muted-foreground">seconds</span>
283 </div>
284 </div>
285​
286 <div className="grid gap-0 border-t sm:grid-cols-2">
287 {BACKGROUND_ACTIVITY_BOOLEAN_OVERRIDES.map(({ key, label }) => (
288 <label
289 key={key}
290 className="flex items-center justify-between gap-3 border-b px-4 py-3 last:border-b-0 sm:border-r sm:even:border-r-0"
291 >
292 <span className="text-sm font-medium">{label}</span>
293 <Switch
294 checked={resolvedBackgroundActivity[key]}
295 onCheckedChange={(checked) =>
296 updateSettings(
297 backgroundActivityOverrideSettings(
298 settings.backgroundActivity,
299 resolvedBackgroundActivity,
300 {
301 [key]: Boolean(checked),
302 },
303 ),
304 )
305 }
306 aria-label={label}
307 />
308 </label>
309 ))}
310 </div>
311 </fieldset>
312 </DialogPanel>
313 <DialogFooter>
314 <Button
315 variant="outline"
316 disabled={!canWriteSettings}
317 onClick={() => updateSettings(resetBackgroundActivitySettings())}
318 >
319 Reset all
320 </Button>
321 <Button onClick={() => onOpenChange(false)}>Done</Button>
322 </DialogFooter>
323 </DialogPopup>
324 </Dialog>
325 );
327​
328export function BackgroundActivitySettingsRow() {
329 const settings = useScopedSettings();
330 const updateSettings = useUpdateScopedSettings();
331 const { scope, environment } = useSettingsScope();
332 const environmentId = environment?.environmentId ?? null;
333 const isEnvironmentScope = scope.environmentIds.length === 1 && environmentId !== null;
334 const [backgroundActivityDialogOpen, setBackgroundActivityDialogOpen] = useState(false);
335 const mixedBackgroundActivity = useScopedSettingsMixed(["backgroundActivity"]);
336 const resolvedBackgroundActivity = resolveServerBackgroundActivitySettings(settings);
337 const activeBackgroundActivityProfile = resolvedBackgroundActivity.profile;
338 const backgroundActivityProfileOption = resolveBackgroundActivityProfileOption(settings);
339 const backgroundActivityDescription =
340 backgroundActivityProfileOption === "advanced"
341 ? `${ADVANCED_BACKGROUND_ACTIVITY_DESCRIPTION} Shared policy: ${
342 BACKGROUND_ACTIVITY_PROFILE_LABELS[activeBackgroundActivityProfile]
343 }.`
344 : BACKGROUND_ACTIVITY_PROFILE_DESCRIPTIONS[resolvedBackgroundActivity.profile];
345 const canResetBackgroundActivity = !Equal.equals(
346 settings.backgroundActivity,
347 DEFAULT_UNIFIED_SETTINGS.backgroundActivity,
348 );
349​
350 return (
351 <SettingsRow
352 serverScoped
353 settingKeys={["backgroundActivity"]}
354 id={searchableSetting("background-activity").id}
355 title={
356 <span className="inline-flex items-center gap-1.5">
357 {searchableSetting("background-activity").title}
358 <PolicyTooltip>
359 This shared policy gates background work such as Git refreshes and provider health
360 probes after their individual intervals elapse.
361 </PolicyTooltip>
362 </span>
363 }
364 description={backgroundActivityDescription}
365 resetAction={
366 canResetBackgroundActivity ? (
367 <SettingResetButton
368 label="background activity"
369 onClick={() => updateSettings(resetBackgroundActivitySettings())}
370 />
371 ) : null
372 }
373 control={
374 <>
375 <Select
376 value={mixedBackgroundActivity ? null : backgroundActivityProfileOption}
377 onValueChange={(value) => {
378 if (value === "advanced") {
379 if (isEnvironmentScope) setBackgroundActivityDialogOpen(true);
380 return;
381 }
382 if (value === "balanced" || value === "performance" || value === "battery-saver") {
383 updateSettings(backgroundActivityProfileSettings(value));
384 }
385 }}
386 >
387 <SelectTrigger
388 size="sm"
389 className="w-full sm:w-40"
390 aria-label="Background activity profile"
391 >
392 <SelectValue>
393 {(value: BackgroundActivityProfileOption | null) =>
394 value === null ? "Mixed" : BACKGROUND_ACTIVITY_PROFILE_OPTION_LABELS[value]
395 }
396 </SelectValue>
397 </SelectTrigger>
398 <SelectPopup align="end" alignItemWithTrigger={false}>
399 <SelectItem hideIndicator value="balanced">
400 {BACKGROUND_ACTIVITY_PROFILE_LABELS.balanced}
401 </SelectItem>
402 <SelectItem hideIndicator value="performance">
403 {BACKGROUND_ACTIVITY_PROFILE_LABELS.performance}
404 </SelectItem>
405 <SelectItem hideIndicator value="battery-saver">
406 {BACKGROUND_ACTIVITY_PROFILE_LABELS["battery-saver"]}
407 </SelectItem>
408 <SelectItem hideIndicator value="advanced" disabled={!isEnvironmentScope}>
409 {isEnvironmentScope
410 ? BACKGROUND_ACTIVITY_PROFILE_OPTION_LABELS.advanced
411 : `${BACKGROUND_ACTIVITY_PROFILE_OPTION_LABELS.advanced} (one environment)`}
412 </SelectItem>
413 </SelectPopup>
414 </Select>
415 {backgroundActivityProfileOption === "advanced" && isEnvironmentScope ? (
416 <Tooltip>
417 <TooltipTrigger
418 render={
419 <Button
420 size="icon-sm"
421 variant="outline"
422 aria-label="Configure advanced background activity"
423 onClick={() => setBackgroundActivityDialogOpen(true)}
424 >
425 <SettingsIcon className="size-4" />
426 </Button>
427 }
428 />
429 <TooltipPopup side="top">Configure background activity</TooltipPopup>
430 </Tooltip>
431 ) : null}
432 <BackgroundActivityAdvancedDialog
433 open={backgroundActivityDialogOpen && isEnvironmentScope}
434 onOpenChange={setBackgroundActivityDialogOpen}
435 />
436 </>
437 }
438 />
439 );