feat: improve custom jiggler settings and add timezone support (#742)

* feat: add timezone support to jiggler and fix custom settings persistence

- Add timezone field to JigglerConfig with comprehensive IANA timezone list
- Fix custom settings not loading current values
- Remove business hours preset, add as examples in custom settings
- Improve error handling for invalid cron expressions

* fix: format jiggler.go with gofmt

* fix: add embedded timezone data and validation

- Import time/tzdata to embed timezone database in binary
- Add timezone validation in runJigglerCronTab() to gracefully fallback to UTC
- Add timezone to debug logging in rpcSetJigglerConfig
- Fixes 'unknown time zone' errors when system lacks timezone data

* refactor: add timezone field comments from jiggler options

* chore: move tzdata to backend

* refactor: fix JigglerSetting linting

- Adjusted useEffect dependency to include send function for better data fetching
- Modified layout classes for improved responsiveness and consistency
- Cleaned up code formatting for better readability

---------

Co-authored-by: Siyuan Miao <i@xswan.net>
This commit is contained in:
Adam Shiervani
2025-08-19 16:50:42 +02:00
committed by GitHub
parent 9f573200b1
commit 8527b1eff1
8 changed files with 831 additions and 42 deletions

View File

@@ -1,44 +1,109 @@
import { useState } from "react";
import { useEffect, useMemo, useState } from "react";
import { LuExternalLink } from "react-icons/lu";
import { Button } from "@components/Button";
import { Button, LinkButton } from "@components/Button";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import { InputFieldWithLabel } from "./InputField";
import ExtLink from "./ExtLink";
import { SelectMenuBasic } from "./SelectMenuBasic";
export interface JigglerConfig {
inactivity_limit_seconds: number;
jitter_percentage: number;
schedule_cron_tab: string;
timezone?: string;
}
export function JigglerSetting({
onSave,
defaultJigglerState,
}: {
onSave: (jigglerConfig: JigglerConfig) => void;
defaultJigglerState?: JigglerConfig;
}) {
const [jigglerConfigState, setJigglerConfigState] = useState<JigglerConfig>({
inactivity_limit_seconds: 20,
jitter_percentage: 0,
schedule_cron_tab: "*/20 * * * * *",
});
const [jigglerConfigState, setJigglerConfigState] = useState<JigglerConfig>(
defaultJigglerState || {
inactivity_limit_seconds: 20,
jitter_percentage: 0,
schedule_cron_tab: "*/20 * * * * *",
timezone: "UTC",
},
);
const [send] = useJsonRpc();
const [timezones, setTimezones] = useState<string[]>([]);
useEffect(() => {
send("getTimezones", {}, resp => {
if ("error" in resp) return;
setTimezones(resp.result as string[]);
});
}, [send]);
const timezoneOptions = useMemo(
() =>
timezones.map((timezone: string) => ({
value: timezone,
label: timezone,
})),
[timezones],
);
const exampleConfigs = [
{
name: "Business Hours 9-17",
config: {
inactivity_limit_seconds: 60,
jitter_percentage: 25,
schedule_cron_tab: "0 * 9-17 * * 1-5",
timezone: jigglerConfigState.timezone || "UTC",
},
},
{
name: "Business Hours 8-17",
config: {
inactivity_limit_seconds: 60,
jitter_percentage: 25,
schedule_cron_tab: "0 * 8-17 * * 1-5",
timezone: jigglerConfigState.timezone || "UTC",
},
},
];
return (
<div className="space-y-2">
<div className="grid max-w-sm grid-cols-1 items-end gap-y-2">
<div className="space-y-4">
<div className="space-y-2">
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
Examples
</h4>
<div className="flex flex-wrap gap-2">
{exampleConfigs.map((example, index) => (
<Button
key={index}
size="XS"
theme="light"
text={example.name}
onClick={() => setJigglerConfigState(example.config)}
/>
))}
<LinkButton
to="https://crontab.guru/examples.html"
size="XS"
theme="light"
text="More examples"
LeadingIcon={LuExternalLink}
/>
</div>
</div>
<div className="grid grid-cols-1 items-end gap-4 md:grid-cols-2">
<InputFieldWithLabel
required
size="SM"
label="Cron Schedule"
description={
<span>
Generate with{" "}
<ExtLink className="text-blue-700 underline" href="https://crontab.guru/">
crontab.guru
</ExtLink>
</span>
}
description="Cron expression for scheduling"
placeholder="*/20 * * * * *"
defaultValue={jigglerConfigState.schedule_cron_tab}
value={jigglerConfigState.schedule_cron_tab}
onChange={e =>
setJigglerConfigState({
...jigglerConfigState,
@@ -50,7 +115,7 @@ export function JigglerSetting({
<InputFieldWithLabel
size="SM"
label="Inactivity Limit Seconds"
description="Seconds of inactivity before triggering a jiggle again"
description="Inactivity time before jiggle"
value={jigglerConfigState.inactivity_limit_seconds}
type="number"
min="1"
@@ -70,7 +135,7 @@ export function JigglerSetting({
description="To avoid recognizable patterns"
placeholder="25"
TrailingElm={<span className="px-2 text-xs text-slate-500">%</span>}
defaultValue={jigglerConfigState.jitter_percentage}
value={jigglerConfigState.jitter_percentage}
type="number"
min="0"
max="100"
@@ -81,9 +146,24 @@ export function JigglerSetting({
})
}
/>
<SelectMenuBasic
size="SM"
label="Timezone"
description="Timezone for cron schedule"
value={jigglerConfigState.timezone || "UTC"}
disabled={timezones.length === 0}
onChange={e =>
setJigglerConfigState({
...jigglerConfigState,
timezone: e.target.value,
})
}
options={timezoneOptions}
/>
</div>
<div className="mt-6 flex gap-x-2">
<div className="flex gap-x-2">
<Button
size="SM"
theme="primary"

View File

@@ -21,6 +21,7 @@ export interface JigglerConfig {
inactivity_limit_seconds: number;
jitter_percentage: number;
schedule_cron_tab: string;
timezone?: string;
}
const jigglerOptions = [
@@ -32,6 +33,8 @@ const jigglerOptions = [
inactivity_limit_seconds: 30,
jitter_percentage: 25,
schedule_cron_tab: "*/30 * * * * *",
// We don't care about the timezone for this preset
// timezone: "UTC",
},
},
{
@@ -41,6 +44,8 @@ const jigglerOptions = [
inactivity_limit_seconds: 60,
jitter_percentage: 25,
schedule_cron_tab: "0 * * * * *",
// We don't care about the timezone for this preset
// timezone: "UTC",
},
},
{
@@ -50,15 +55,8 @@ const jigglerOptions = [
inactivity_limit_seconds: 300,
jitter_percentage: 25,
schedule_cron_tab: "0 */5 * * * *",
},
},
{
value: "business_hours",
label: "Business Hours - 1m - (Mon-Fri 9-17)",
config: {
inactivity_limit_seconds: 60,
jitter_percentage: 25,
schedule_cron_tab: "0 * 9-17 * * 1-5",
// We don't care about the timezone for this preset
// timezone: "UTC",
},
},
] as const;
@@ -77,6 +75,9 @@ export default function SettingsMouseRoute() {
const [selectedJigglerOption, setSelectedJigglerOption] =
useState<JigglerValues | null>(null);
const [currentJigglerConfig, setCurrentJigglerConfig] = useState<JigglerConfig | null>(
null,
);
const scrollThrottlingOptions = [
{ value: "0", label: "Off" },
@@ -99,6 +100,8 @@ export default function SettingsMouseRoute() {
send("getJigglerConfig", {}, resp => {
if ("error" in resp) return;
const result = resp.result as JigglerConfig;
setCurrentJigglerConfig(result);
const value = jigglerOptions.find(
o =>
o?.config?.inactivity_limit_seconds === result.inactivity_limit_seconds &&
@@ -128,9 +131,20 @@ export default function SettingsMouseRoute() {
send("setJigglerConfig", { jigglerConfig }, async resp => {
if ("error" in resp) {
return notifications.error(
`Failed to set jiggler config: ${resp.error.data || "Unknown error"}`,
);
const errorMsg = resp.error.data || "Unknown error";
// Check for cron syntax errors and provide user-friendly message
if (
errorMsg.includes("invalid syntax") ||
errorMsg.includes("parse failure") ||
errorMsg.includes("invalid cron")
) {
return notifications.error(
"Invalid cron expression. Please check your schedule format (e.g., '0 * * * * *' for every minute).",
);
}
return notifications.error(`Failed to set jiggler config: ${errorMsg}`);
}
notifications.success(`Jiggler Config successfully updated`);
@@ -202,10 +216,7 @@ export default function SettingsMouseRoute() {
/>
</SettingsItem>
<SettingsItem
title="Jiggler"
description="Simulate movement of a computer mouse. Prevents sleep mode, standby mode or the screensaver from activating"
>
<SettingsItem title="Jiggler" description="Simulate movement of a computer mouse">
<SelectMenuBasic
size="SM"
label=""
@@ -222,13 +233,15 @@ export default function SettingsMouseRoute() {
e.target.value as (typeof jigglerOptions)[number]["value"],
);
}}
fullWidth
/>
</SettingsItem>
{selectedJigglerOption === "custom" && (
<SettingsNestedSection>
<JigglerSetting onSave={saveJigglerConfig} />
<JigglerSetting
onSave={saveJigglerConfig}
defaultJigglerState={currentJigglerConfig || undefined}
/>
</SettingsNestedSection>
)}
<div className="space-y-4">